Пишемо DXE-драйвер для зняття скріншотів з BIOS Setup і інших UEFI-додатків

У минулій статті про SecureBoot мені дуже не вистачало можливості зробити знімок екрану при налаштуванні UEFI BIOS Setup, але тоді виручило перенаправлення текстової консолі в послідовний порт. Це відмінне рішення, але доступно воно на небагатьох серверних материнських платах, і через нього можна отримати лише псевдографику, а хотілося б отримати справжню — вона і виглядає приємніше, і вирізати її кожен раз з вікна термінала не треба.
Ось саме цим ми і займемося в цій статті, а заодно я розповім, що таке DXE-драйвер і як написати, зібрати і протестувати такий самостійно, як працюють введення з клавіатури і виведення на екран в UEFI, як знайти серед підключених пристроїв зберігання таке, на яке можна записувати файли, як зберегти що-небудь у файл з UEFI і як адаптувати якийсь зовнішній код на С для роботи у складі прошивки.
Якщо вам все ще цікаво — чекаю вас під катом.


Відмова від відповідальності

Перш ніж говорити про написання і налагодження драйверів для UEFI, варто відразу сказати, що експерименти з прошивкою — справа небезпечна, вони можуть призвести до «цеглини», а в найбільш невдалих рідкісних випадках — до виходу з ладу апаратури, тому я заздалегідь попереджаю: все, що ви тут прочитаєте, ви використовуєте на свій страх і ризик, я не несу і не буду нести відповідальність за втрату працездатності вашої прошивки або плати. Перш ніж починати будь-які експерименти з прошивкою, необхідно зробити повну копію всього вмісту SPI flash за допомогою програматора. Тільки так ви можете гарантувати успішне відновлення прошивки після будь-якого програмного збою.
Якщо у вас немає програматора, але спробувати написати і відлагодити DXE-драйвер дуже хочеться, використовуйте для цього OVMF, VmWare Workstation 12 або будь-які інші системи віртуалізації з підтримкою UEFI на ваш вибір.

Що там потрібно і чому це DXE-драйвер

Завдання наше полягає в тому, щоб зняти скріншот всього екрану під час роботи якогось UEFI-додатки, наприклад BIOS Setup, натисненням певної комбінації клавіш, знайти файлову систему з доступом на запис і зберегти отриманий скріншот на неї. Також було б непогано отримати якусь індикацію статусу. Т. к. для зняття скріншота потрібно переривати роботу UEFI-додатків, сама програма щодо їх зняття додатком бути не може, адже ніякої витісняючої багатозадачності в UEFI поки ще не передбачено, тому нам потрібен DXE-драйвер.
Схема його роботи планується приблизно наступна:
0. Завантажується тільки після появи текстового вводу (щоб обробляти натискання комбінації клавіш) і графічного виводу (щоб було з чого знімати скріншоти).
1. Вішаємо оброблювач натискання комбінації LCtrl + LAlt + F12 (або будь-який інший на ваш смак) на всі доступні вхідні текстові консолі.
2. В процесорі знаходимо всі вихідні графічні консолі, робимо з них скріншот і перекодируем його у формат PNG (т. к. UEFI-зазвичай не використовують мільйони квітів, то в цьому форматі скріншоти виходять розміром в десятки кілобайт замість декількох мегабайт в BMP).
3. У тому ж процесорі знаходимо першу-ліпшу ФС з можливістю запису в корінь і зберігаємо туди отримані файли.
Можна розширити функціональність вибором не першій-ліпшій ФС, а, наприклад, тільки USB-пристроїв або тільки розділів ESP, залишимо це на самостійну роботу читачеві.

Вибираємо SDK

Для написання нового коду для роботи в UEFI є два різних SDK — більш новий EDK2 від UEFI Forum GNU-EFI від незалежних розробників, заснований на старому коді Intel. Обидва рішення мають на увазі, що ви будете писати код на C та/або асемблері, в нашому випадку постараємося обійтися чистим C.
Не мені судити, який SDK краще, але я пропоную використовувати EDK2, т. к. він офіційний і багатоплатформовий, і нові фічі (разом з виправленням старих багів) з'являються в ньому значно швидше завдяки близькості до джерела змін, плюс саме його використовують всі відомі мені IBV для написання свого коду.
EDK2 знаходиться в процесі постійної розробки, і в його trunk стабільно додають по 2-3 коміта в день, але так як ми тут за останніми віяннями не женемося (все одно вони ще ні в кого не працюють), тому будемо використовувати останній на даний момент стабільний зріз EDK2, який називається UDK2015.
Щоб забезпечити кросплатформеність і можливість зборки різними компіляторами, EDK2 генерує make-файли для кожної платформи, використовуючи конфігураційні файли TXT (конфігурація оточення), DEC, DSC і FDF (конфігурація пакета) і INF (конфігурація компонента), докладніше про них я розповім по ходу розповіді, а зараз потрібно дістати EDK2 і зібрати HelloWorld, чим і займемося, якщо ж вам не терпиться дізнатися подробиці прямо зараз — зверніться до документації.

Налаштовуємо складальне оточення

Мається на увазі, що потрібне для збірки коду на C і асемблері ЗА вже встановлено на вашій машині. Якщо ні, користувачам Windows пропоную встановити Visual Studio 2013 Express for Windows Desktop, користувачам Linux і OSX знадобляться GCC 4.4-4.9 NASM.
Якщо все це вже встановлено, залишилося тільки скачати UDK2015, розпакувати вміст UDK2015.MyWorkSpace.zip туди, де у вас є право на створення файлів (хоч прямо на робочий стіл або в домашню директорію), а потім розпакувати вміст BaseTools(Windows).zip або BaseTools(Unix.zip) в отриману на попередньому кроці директорію MyWorkSpace, яку потім перейменувати в щось пристойне, наприклад, UDK2015.
Тепер відкриваємо термінал, переходимо в тільки що створену директорію UDK2015 і виконуємо там скрипт edksetup.bat (або .sh), який скопіює в підпапку Conf набір текстових файлів, нас будуть цікавити tools_def.txt target.txt.
Перший файл досить великий, у ньому містяться визначення змінних оточення з шляхами до необхідних складальному оточенню компіляторів C і ASL, асемблерів, линковщиков і т. п. Якщо вам потрібно, можете виправити вказані там шляху або додати свій набір утиліт (т. зв. ToolChain), але якщо ви послухали моєї ради, то вам без змін підійде або VS2013 (якщо у вас 32-розрядна Windows), або VS2013x86 (у разі 64-розрядної Windows), або GCC44 |… | GCC49 (в залежності від вашої версії GCC, яку той люб'язно показує у відповідь на gcc --version).
У другому файлі містяться налаштування збірки за замовчуванням, в ньому я рекомендую встановити наступні значення:
ACTIVE_PLATFROM = MdeModulePkg/MdeModulePkg.dsc # Основний пакет для розробки модулів
TARGET = RELEASE # Релізна конфігурація
TARGET_ARCH = X64 # DXE на більшості сучасних машин 64-бітна, винятки дуже рідкісні і дуже болючі
TOOL_CHAN_TAG = VS2013x86 # | VS2013 | GCC44 | ... | GCC49 | YOUR_FANCY_TOOLCHAIN, виберіть найбільш підходящий у вашому випадку
Відкрийте ще один термінал в UDK2015 і в Linux/OSX виконайте команду:
. edksetup.sh BaseTools
У разі Windows достатньо звичайного edksetup.bat без параметрів.
Тепер протестуємо складальне оточення командою build, якщо все було зроблено вірно, то після певного часу закінчиться повідомленням на кшталт
- Done -
Build end time: ...
Build total time: ...
Якщо ж замість Done ви бачите Failed, значить з вашими налаштуваннями щось не так. Я перевірив вищевказане на VS2013x86 в Windows і GCC48 в Xubuntu 14.04.3 — УМВР.

Структура проекту

Додатки і драйвери в EDK2 збираються не окремо, а у складі т. н Package, тобто пакета. У пакет, крім самих додатків, входять ще й бібліотеки, набори заголовних файлів і файли з описом конфігурації пакету і його вмісту. Зроблено це для того, щоб дозволити різним драйверів і додатків використовувати різні реалізації бібліотек, мати доступ до різних заголовочным файлів і GUID'ам. Ми будемо використовувати MdeModulePkg, це дуже загальний пакет без яких-небудь залежностей від архітектури і заліза, і якщо наш драйвер вдасться зібрати в ньому, він майже гарантовано буде працювати на будь-яких реалізаціях UEFI 2.1 нових. Недоліком такого підходу є те, що велика частина бібліотек в ньому (наприклад, DebugLib, використовувана для отримання налагоджувального виведення) — просто заглушки, і їх доведеться писати самому, якщо виникне така необхідність.
Для складання нашого драйвера знадобиться INF-файл з інформацією про те, які саме бібліотеки, протоколи і файли йому потрібні для складання, а також додавання шляху до цього INF-файлу в DSC-файл пакета, щоб складальна система взагалі знала, що такий файл INF є.
Почнемо з кінця: відкриваємо файл UDK2015/MdeModulePkg/MdeModulePkg.dsc і перегортаємо його до розділу [Components] (можна знайти його пошуком — це швидше). У розділі наведено по порядку всі файли пакету, виглядає початок розділу ось так:
[Components]
MdeModulePkg/Application/HelloWorld/HelloWorld.inf
MdeModulePkg/Application/MemoryProfileInfo/MemoryProfileInfo.inf
...
Додаємо туди свій майбутній INF-файл разом з шляхом до нього щодо UDK2015. Пропоную створити для нього прямо в MdeModulePkg папку CrScreenshotDxe, а сам файл INF назвати CrScreenshotDxe.inf. Як ви вже здогадалися, Cr — це від «CodeRush», а автор цієї статті — сама скромність. В результаті вийде щось таке:
[Components]
MdeModulePkg/CrScreenshotDxe/CrScreenshotDxe.inf
MdeModulePkg/Application/HelloWorld/HelloWorld.inf
MdeModulePkg/Application/MemoryProfileInfo/MemoryProfileInfo.inf
...
Зберігаємо зміни і закриваємо DSC-файл, більше ми його змінювати не будемо, якщо не захочемо налаштувати вивід зневадження, але це вже зовсім інша історія.
Тепер потрібно заповнити сам INF-файл:
Виглядати він буде приблизно так
[Defines] # Основні визначення
INF_VERSION = 0x00010005 # Версія специфікації, нам досить 1.5
BASE_NAME = CrScreenshotDxe # Назва компонента
FILE_GUID = cab058df-e938-4f85-8978-1f7e6aabdb96 # компонент GUID
MODULE_TYPE = DXE_DRIVER # Тип компонента
VERSION_STRING = 1.0 # Версія компонента
ENTRY_POINT = CrScreenshotDxeEntry # назва точки входу

[Sources.common] # Файли для збірки, common - загальні для всіх арзитектур 
CrScreenshotDxe.c # Код нашого драйвера
#... # Може бути, нам знадобиться щось ще, в PNG конвертер, наприклад

[Packages] # Використовуються пакети 
MdePkg/MdePkg.dec # Основний пакет, без нього не обходиться ні один компонент UEFI
MdeModulePkg/MdeModulePkg.dec # Другий основний пакет, потрібний драйверів і додатків

[LibraryClasses] # Використовуються бібліотеки
UefiBootServicesTableLib # Зручний доступ до UEFI Boot Services через покажчик gBS
UefiRuntimeServicesTableLib # Не менш зручний доступ до UEFI Runtime services через покажчик gRT
UefiDriverEntryPoint # Точка входу в UEFI-драйвер, без неї конструктори бібліотек не спрацюють, а вони потрібні
DebugLib # макросу DEBUG
PrintLib # Для UnicodeSPrint, місцевого аналога snprintf

[Protocols] # Використовуються протоколи
gEfiGraphicsOutputProtocolGuid # Доступ до графічної консолі
gEfiSimpleTextInputExProtocolGuid # Доступ до текстового вводу
gEfiSimpleFileSystemProtocolGuid # Доступ до файлових систем

[Depex] # Залежно драйвера, поки ці протоколи недоступні, драйвер не запуститься
gEfiGraphicsOutputProtocolGuid AND # Доступ до ФС для запуску не обов'язковий, потім перевіримо його наявність в рантайме
gEfiSimpleTextInputExProtocolGuid # 
Залишилося створити згаданий вище файл CrScreenshotDxe.:ось З таким вмістом
#include <Uefi.h>
#include <Library/DebugLib.h>
#include <Library/PrintLib.h>
#include <Library/UefiDriverEntryPoint.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiRuntimeServicesTableLib.h>
#include <Protocol/GraphicsOutput.h>
#include <Protocol/SimpleTextInEx.h>
#include <Protocol/SimpleFileSystem.h>

EFI_STATUS
EFIAPI
CrScreenshotDxeEntry (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
return EFI_SUCCESS;
}
Якщо тепер повторити команду build, вона повинна бути успішною, інакше ви щось зробили неправильно.
Ось тепер у нас, нарешті, є заготовка для нашого драйвера, можна перейти безпосередньо до написання коду. Абсолютно ясно, що така складальна система нікуди не годиться, і працювати з нею через редагування текстових файлів не дуже приємно, тому кожен з IBV має власне рішення щодо інтеграції складальної системи EDK2 в якусь сучасну IDE, наприклад середовище AMI Visual eBIOS — це такий обвішана плагінами Eclipse, а Phoenix і Insyde обважують ними ж Visual Studio.
Є ще чудовий проект VisualUefi за авторством відомого фахівця з комп'ютерної безпеки Алекса Іонеску, і якщо ви теж любите Visual Studio — пропоную спробувати його, а ми поки продовжимо угарать по хардкору, підтримувати дух старої школи і все таке.

Реагуємо на натискання комбінації клавіш

Тут все досить просто: при завантаженні драйвера переберемо всі примірники протоколу SimpleTextInputEx, який публікується драйвером клавіатури і найчастіше рівно один, навіть у разі, коли до системи підключено декілька клавіатур — буфер то загальний, якщо спеціально щось не міняти. Тим не менш, на всякий випадок переберемо всі доступні екземпляри, викликавши у кожного функцію RegisterKeyNotify, яка
в якості параметра приймає комбінацію клавіш, яку ми маємо намір реагувати, і покажчик на callback функцію, яка буде викликана після натискання потрібно комбінації, а в ній вже і буде проведена вся основна робота.
Переводимо з російської на З
EFI_STATUS
EFIAPI
CrScreenshotDxeEntry (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
EFI_STATUS Status;
EFI_KEY_DATA KeyStroke;
UINTN HandleCount;
EFI_HANDLE *HandleBuffer = NULL;
UINTN i;

// Set keystroke to be LCtrl+LAlt+F12
KeyStroke.Key.ScanCode = SCAN_F12;
KeyStroke.Key.UnicodeChar = 0;
KeyStroke.KeyState.KeyShiftState = EFI_SHIFT_STATE_VALID | EFI_LEFT_CONTROL_PRESSED | EFI_LEFT_ALT_PRESSED;
KeyStroke.KeyState.KeyToggleState = 0;

// Locate all SimpleTextInEx protocols
Status = gBS->LocateHandleBuffer(ByProtocol, &gEfiSimpleTextInputExProtocolGuid, NULL, &HandleCount, &HandleBuffer);
if (EFI_ERROR (Status)) {
DEBUG((-1, "CrScreenshotDxeEntry: gBS->LocateHandleBuffer returned %r\n", Status));
return EFI_UNSUPPORTED;
}

// For each instance
for (i = 0; i < HandleCount; i++) {
EFI_HANDLE Handle;
EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *SimpleTextInEx;

// Get handle protocol
Status = gBS->HandleProtocol (HandleBuffer[i], &gEfiSimpleTextInputExProtocolGuid, (VOID **) &SimpleTextInEx);
if (EFI_ERROR (Status)) {
DEBUG((-1, "CrScreenshotDxeEntry: gBS->HandleProtocol[%d] returned %r\n", i, Status));
continue;
}

// Register key notification function
Status = SimpleTextInEx->RegisterKeyNotify(
SimpleTextInEx, 
&KeyStroke, 
TakeScreenshot, 
&Handle);
if (EFI_ERROR (Status)) {
DEBUG((-1, "CrScreenshotDxeEntry: SimpleTextInEx->RegisterKeyNotify[%d] returned %r\n", i, Status));
}
}

// Free memory used for handle buffer
if (HandleBuffer)
gBS->FreePool(HandleBuffer);

// Show driver loaded
ShowStatus(0xFF, 0xFF, 0xFF); // White

return EFI_SUCCESS;
}
Для успішної компіляції поки не вистачає функцій TakeScreenshot ShowStatus, про які нижче.

Шукаємо ФС з доступом на запис, пишемо дані в файл

Перш, ніж шукати доступні графічні консолі і знімати з них скріншоти, потрібно з'ясувати, чи можна ці самі скріншоти кудись зберегти. Для цього потрібно знайти усі примірники протоколу SimpleFileSystem, який публікується драйвером PartitionDxe для кожного виявленого тома, ФС якого відома прошивці. Найчастіше єдині відомі ФС — сімейство FAT12/16/32 (іноді тільки FAT32), які за стандартом UEFI можуть використовуватися для ESP. Далі потрібно перевірити, що на знайдену ФС можлива запис, зробити це можна різними способами, найпростіший — спробувати створити на ній файл і відкрити його на читання і запис, якщо вийшло — на цю ФС можна писати. Рішення, звичайно, не оптимальне, але працює, правильну реалізацію пропоную читачам як вправи.
Знову переводимо з російської на З
EFI_STATUS
EFIAPI
FindWritableFs (
OUT EFI_FILE_PROTOCOL **WritableFs
)
{
EFI_HANDLE *HandleBuffer = NULL;
UINTN HandleCount;
UINTN i;

// Locate all the simple file system devices in the system
EFI_STATUS Status = gBS->LocateHandleBuffer(ByProtocol, &gEfiSimpleFileSystemProtocolGuid, NULL, &HandleCount, &HandleBuffer);
if (!EFI_ERROR (Status)) {
EFI_FILE_PROTOCOL *Fs = NULL;
// Located For each volume
for (i = 0; i < HandleCount; i++) {
EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *SimpleFs = NULL;
EFI_FILE_PROTOCOL *File = NULL;

// Get protocol pointer for current volume
Status = gBS->HandleProtocol(HandleBuffer[i], &gEfiSimpleFileSystemProtocolGuid, (VOID **) &SimpleFs);
if (EFI_ERROR (Status)) {
DEBUG((-1, "FindWritableFs: gBS->HandleProtocol[%d] returned %r\n", i, Status));
continue;
}

// Open the volume
Status = SimpleFs->OpenVolume(SimpleFs, &Fs);
if (EFI_ERROR (Status)) {
DEBUG((-1, "FindWritableFs: SimpleFs->OpenVolume[%d] returned %r\n", i, Status));
continue;
}

// Try opening a file for writing
Status = Fs->Open(Fs, &File, L"crsdtest.fil", EFI_FILE_MODE_CREATE | EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE, 0);
if (EFI_ERROR (Status)) {
DEBUG((-1, "FindWritableFs: Fs->Open[%d] returned %r\n", i, Status));
continue;
}

// Writable FS found
Fs->Delete(File);
*WritableFs = Fs;
Status = EFI_SUCCESS;
break;
}
}

// Free memory
if (HandleBuffer) {
gBS->FreePool(HandleBuffer);
}

return Status;
}
Цього коду більше нічого не потрібно, працює як є.

Шукаємо графічну консоль і робимо знімок її екрана

Перевіривши, що зберігати скріншоти є на що, займемося їх зняттям. Для цього знадобиться перебрати усі примірники протоколу GOP, який публікують GOP-драйвери і VideoBIOS'и (точніше, не сам VBIOS, який нічого не знає ні про які протоколи, а драйвер ConSplitter, що реалізує прошарок між старими VBIOS і UEFI) для кожного пристрою виводу з графікою. У цього пртокола є функція Blt для копіювання зображення з фреймбуффера і в нього, поки нам знадобиться тільки перше. За допомогою об'єкта Mode того ж протоколу можна отримати поточний дозвіл екрану, яке потрібно для виділення буффера потрібного розміру і зняття скріншоту всього екрана, а не з якоїсь його частини. отримавши скріншот, варто перевірити що він не абсолютно чорний, бо зберігати такі — зайва трата часу і місця на ФС, чорний прямокутник потрібного розміру можна і в Paint намалювати. Потім потрібно перетворити картинку з BGR (в якому її віддає Blt) RGB (який потрібен энкодеру PNG) інакше кольору на скріншотах будуть неправильні. Кодуємо отриману після конвертації картинку і зберігаємо її в файл на тій ФС, яку ми знайшли на попередньому кроці. Ім'я файлу у форматі 8.3 зберемо з поточної дати і часу, так менше шанс, що один скріншот перепише інший.
Знову переводимо з російської на З
EFI_STATUS
EFIAPI
TakeScreenshot (
IN EFI_KEY_DATA *KeyData
)
{
EFI_FILE_PROTOCOL *Fs = NULL;
EFI_FILE_PROTOCOL *File = NULL;
EFI_GRAPHICS_OUTPUT_PROTOCOL *GraphicsOutput = NULL;
EFI_GRAPHICS_OUTPUT_BLT_PIXEL *Image = NULL;
UINTN ImageSize; // Size in pixels
UINT8 *PngFile = NULL;
UINTN PngFileSize; // Size in bytes
EFI_STATUS Status;
UINTN HandleCount;
EFI_HANDLE *HandleBuffer = NULL;
UINT32 ScreenWidth;
UINT32 ScreenHeight;
CHAR16 FileName[8+1+3+1]; // 0-terminated 8.3 file name
EFI_TIME Time;
UINTN i, j;

// Find writable FS
Status = FindWritableFs(&Fs);
if (EFI_ERROR (Status)) {
DEBUG((-1, "TakeScreenshot: can't find writable FS\n"));
ShowStatus(0xFF, 0xFF, 0x00); // Yellow
return EFI_SUCCESS;
}

// Locate all instances of GOP
Status = gBS->LocateHandleBuffer(ByProtocol, &gEfiGraphicsOutputProtocolGuid, NULL, &HandleCount, &HandleBuffer);
if (EFI_ERROR (Status)) {
DEBUG((-1, "ShowStatus: Graphics output protocol not found\n"));
return EFI_SUCCESS;
}

// For each GOP instance
for (i = 0; i < HandleCount; i++) {
do { // Break from do used instead of "goto error"
// Handle protocol
Status = gBS->HandleProtocol(HandleBuffer[i], &gEfiGraphicsOutputProtocolGuid, (VOID **) &GraphicsOutput);
if (EFI_ERROR (Status)) {
DEBUG((-1, "ShowStatus: gBS->HandleProtocol[%d] returned %r\n", i, Status));
break;
}

// Set screen width, height and image size in pixels
ScreenWidth = GraphicsOutput->Mode->Info->HorizontalResolution;
ScreenHeight = GraphicsOutput->Mode->Info->VerticalResolution;
ImageSize = ScreenWidth * ScreenHeight;

// Get current time
Status = gRT->GetTime(&Time, NULL);
if (!EFI_ERROR(Status)) {
// Set file name to current and day time
UnicodeSPrint(FileName, 26, L"%02d%02d%02d%02d.png", Time.Day, Time.Hour Time.Minute, Time.Second);
}
else {
// Set file name to scrnshot.png
UnicodeSPrint(FileName, 26, L"scrnshot.png");
}

// Allocate memory for screenshot
Status = gBS->AllocatePool(EfiBootServicesData, ImageSize * sizeof(EFI_GRAPHICS_OUTPUT_BLT_PIXEL), (VOID **)&Image);
if (EFI_ERROR(Status)) {
DEBUG((-1, "TakeScreenshot: gBS->AllocatePool returned %r\n", Status));
break;
}

// Take screenshot
Status = GraphicsOutput->Blt(GraphicsOutput, Image, EfiBltVideoToBltBuffer, 0, 0, 0, 0, ScreenWidth, ScreenHeight, 0);
if (EFI_ERROR(Status)) {
DEBUG((-1, "TakeScreenshot: GraphicsOutput->Blt returned %r\n", Status));
break;
}

// Check for pitch black image (it means we are using a wrong GOP)
for (j = 0; j < ImageSize; j++) {
if (Image[j].Red != 0x00 || Image[j].Green != 0x00 || Image[j].Blue != 0x00)
break;
}
if (j == ImageSize) {
DEBUG((-1, "TakeScreenshot: GraphicsOutput->Blt returned pitch black image, skipped\n"));
ShowStatus(0x00, 0x00, 0xFF); // Blue
break;
}

// Open or create output file
Status = Fs->Open(Fs, &File, FileName, EFI_FILE_MODE_CREATE | EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE, 0);
if (EFI_ERROR (Status)) {
DEBUG((-1, "TakeScreenshot: Fs->Open of %s returned %r\n", FileName, Status));
break;
}

// Convert BGR to RGBA with Alpha set to 0xFF
for (j = 0; j < ImageSize; j++){
UINT8 Temp = Image[j].Blue;
Image[j].Blue = Image[j].Red;
Image[j].Red = Temp;
Image[j].Reserved = 0xFF;
}

// Encode raw RGB image to format PNG
j = lodepng_encode32(&PngFile, &PngFileSize, (CONST UINT8*)Image, ScreenWidth, ScreenHeight);
if (j) {
DEBUG((-1, "TakeScreenshot: lodepng_encode32 returned %d\n", j));
break;
}

// Write PNG image into the file and close it
Status = File->Write(File, &PngFileSize, PngFile);
File->Close(File);
if (EFI_ERROR(Status)) {
DEBUG((-1, "TakeScreenshot: File->Write returned %r\n", Status));
break;
}

// Show success
ShowStatus(0x00, 0xFF, 0x00); // Green
} while(0);

// Free memory
if (Image)
gBS->FreePool(Image);
if (PngFile)
gBS->FreePool(PngFile);
Image = NULL;
PngFile = NULL;
}

// Show error
if (EFI_ERROR(Status))
ShowStatus(0xFF, 0x00, 0x00); // Red

return EFI_SUCCESS;
}
Для роботи не вистачає lodepng_encode32 та вже згадуваної вище ShowStatus, продовжимо.

Кодуємо зображення у формат PNG

Кращий спосіб писати код — не писати його, тому візьмемо готову бібліотеку для кодування і декодування PNG по імені lodepng. Качаємо, кладемо поряд з нашим З файлом, додаємо наш INF-файл розділ [Sources.common] рядка lodepng.h lodepng.c, включаємо заголовковий файл, дів… нічого не компілюється, т. до lodepng не очікує, що стандартна бібліотека мови C може ось так брати і відсутнім. Нічого, допилим, не вперше.
На початок lodepng.h додамо наступне:
#include <Uefi.h> // Для успішної збірки в середовищі UEFI
#define LODEPNG_NO_COMPILE_DECODER // Відключаємо декодер PNG
#define LODEPNG_NO_COMPILE_DISK // Відключаємо запис на диск, т. к. fopen/fwrite у нас немає
#define LODEPNG_NO_COMPILE_ALLOCATORS // Відключаємо стандартні malloc/realloc/free, т. к. їх у нас немає
#define LODEPNG_NO_COMPILE_ERROR_TEXT // Відключаємо повідомлення про помилки 
#define LODEPNG_NO_COMPILE_ANCILLARY_CHUNKS // Відключаємо текстові дані в PNG, т. к. не потрібні
#if !defined(_MSC_VER) // Визначаємо тип size_t для GCC, MS він вбудований при налаштуваннях складання за замовчуванням
#define size_t UINTN
#endif
І закомментіруем рядок #include < string.h>, якого у нас теж немає. Можна, звичайно, створити локальний файл з тим же ім'ям, визначивши там тип size_t, але раз вже почали міняти — будемо міняти.
lodepng.c трохи складніше, оскільки із стандартної бібліотеки, крім size_t, йому також потрібні memset, memcpy, malloc, realloc, free, qsort, а ще він використовує обчислення з плаваючою точкою. Реалізацію qsort можна поцупити у Apple, функції роботи з пам'яттю зробити обгортками над gBS->CopyMem, gBS->SetMem, gBS->AllocatePool і gBS->FreePool відповідно, а для того, щоб сигналізувати про роботу з FPU потрібно визначити константу CONST INT32 _fltused = 0;, інакше програма компонування буде лаятися на її відсутність. Про коментування файлів зі стандартними #include'ами я вже не кажу — все і так зрозуміло.
Аналогічним чином до нормального бою наводиться qsort.c, не забудьте тільки додати його в INF-файл.

Виводимо статус

Залишилося написати функцію ShowStatus і наш драйвер готовий. Отримувати цей статус можна різними способами, наприклад, виводити числа від 0x00 до 0xFF в CPU IO-порт 80h, який підключений до POST-кодеру, але він є далеко не у всіх, а на ноутбуках — взагалі не зустрічається. Можна пищати спікером, але це, по-перше, платформо-залежна, а по-друге — дико бісить вже після пари скріншотів. Можна блимати лампочками на клавіатурі, це додаткове завдання для читача, а ми будемо показувати статус роботи з графічної консолі прямо через цю графічну консоль — відображаючи маленький квадрат потрібного кольору в лівому верхньому кутку екрану. При цьому білий квадрат буде означати «драйвер успішно завантажений», жовтий — «ФС з можливістю запису не знайдена», синій — «Скріншот поточної консолі повністю чорний, зберігати немає сенсу», червоний — «помилка» і, нарешті, зелений — «скріншот знятий і збережений». Виводити це квадрат потрібно на всі консолі, а після короткого часу відновлювати той шматочок зображення, який їм був затертий.
востаннє переводимо з російської на З
EFI_STATUS
EFIAPI
ShowStatus (
IN UINT8 Red, 
IN UINT8 Green, 
IN UINT8 Blue
)
{
// Determines the size of status square
#define STATUS_SQUARE_SIDE 5

UINTN HandleCount;
EFI_HANDLE *HandleBuffer = NULL;
EFI_GRAPHICS_OUTPUT_PROTOCOL *GraphicsOutput = NULL;
EFI_GRAPHICS_OUTPUT_BLT_PIXEL Square[STATUS_SQUARE_SIDE * STATUS_SQUARE_SIDE];
EFI_GRAPHICS_OUTPUT_BLT_PIXEL Backup[STATUS_SQUARE_SIDE * STATUS_SQUARE_SIDE];
UINTN i;

// Locate all instances of GOP
EFI_STATUS Status = gBS->LocateHandleBuffer(ByProtocol, &gEfiGraphicsOutputProtocolGuid, NULL, &HandleCount, &HandleBuffer);
if (EFI_ERROR (Status)) {
DEBUG((-1, "ShowStatus: Graphics output protocol not found\n"));
return EFI_UNSUPPORTED;
}

// Set color square
for (i = 0 ; i < STATUS_SQUARE_SIDE * STATUS_SQUARE_SIDE; i++) {
Square[i].Blue = Blue;
Square[i].Green = Green;
Square[i].Red = Red;
Square[i].Reserved = 0x00;
}

// For each GOP instance
for (i = 0; i < HandleCount; i ++) {
// Handle protocol
Status = gBS->HandleProtocol(HandleBuffer[i], &gEfiGraphicsOutputProtocolGuid, (VOID **) &GraphicsOutput);
if (EFI_ERROR (Status)) {
DEBUG((-1, "ShowStatus: gBS->HandleProtocol[%d] returned %r\n", i, Status));
continue;
}

// Backup current image
GraphicsOutput->Blt(GraphicsOutput, Backup, EfiBltVideoToBltBuffer, 0, 0, 0, 0, STATUS_SQUARE_SIDE, STATUS_SQUARE_SIDE, 0);

// Draw the status square
GraphicsOutput->Blt(GraphicsOutput, Square, EfiBltBufferToVideo, 0, 0, 0, 0, STATUS_SQUARE_SIDE, STATUS_SQUARE_SIDE, 0);

// Wait 500ms
gBS->Stall(500*1000);

// Restore the backup
GraphicsOutput->Blt(GraphicsOutput, Backup, EfiBltBufferToVideo, 0, 0, 0, 0, STATUS_SQUARE_SIDE, STATUS_SQUARE_SIDE, 0);
}

return EFI_SUCCESS;
}
Ось тепер все готово і успішно збирається, якщо немає — пиляйте, поки не збереться, або скачайте мій готовий драйвер з GitHub і порівняйте з вашим, може бути я якісь зміни банально забув описати.

Тестуємо результат в UEFI Shell

Забираємо наш зібраний драйвер UDK2015/Build/MdeModulePkg/RELEASE/X64/MdeModulePkg/CrScreenshotDxe/CrScreenshotDxe/OUTPUT, знадобляться нам звідти тільки два файла — сам драйвер CrScreenshotDxe.efi і секція залежностей для нього CrScreenshotDxe.depex
Для початку протестуємо роботу драйвера з UEFI Shell. Скопіюйте файл CrScreenshotDxe.efi на USB-флешку з UEFI Shell, завантажитеся в нього, перейдіть в корінь флешки командою fs0: (номер може змінюватися в залежності від кількості підключених до вашої системі дисків) і виконайте команду load CrScreenshotDxe.efi. Якщо побачили повідомлення про успіх і промелькнувший у верхньому кутку екрану білий квадрат — значить драйвер завантажений і працює. У мене це виглядає ось так:
UEFI Shell
Цей скріншот, як і всі наступні, знятий нашим драйвером, тому квадрата в кутку на ньому не видно.
Далі сміливо тисніть LCtrl + LAlt + F12 і спостерігайте за статусом. На моїх системах з AMI графічна консоль одна, і тому я бачу промелькнувший зелений квадрат і отримую один скріншот за одне натискання комбінації. На моїх системах з Phoenix і Insyde виявилося по дві графічні консолі, одна з яких порожня, тому я бачу спочатку синій квадрат, а потім зелений, скріншот при цьому теж тільки один. Результат тестування з UEFI Shell на них виглядає так само, тільки дозвіл там вже не 800х600, а 1366х768.
Ну от, з шелла все працює і можна знімати скріншоти з UEFI-додатків, ось такі:
RU.efi

Тестуємо результат модифікованої прошивки

На жаль, скріншот з BIOS Setup таким чином не зняти — драйвер завантажується надто пізно. Рішень можливих тут два, перше — додати наш драйвер разом з секцією залежностей в DXE-те прошивки за допомогою UEFITool, другий — додати його до OptionROM якого-небудь PCIe-пристрою, тоді і модифікація прошивки не знадобиться. Другий спосіб я ще спробую реалізувати пізніше, коли отримаю потрібну залізяку, а от з першим проблем ніяких немає. Вставляємо, шиємо, стартуємо, встромляємо флешку, заходимо в BIOS Setup, натискаємо LCtrl + LAlt + F12 — вуаля, бачимо синій і зелений квадрати, все працює. Виглядає результат ось так:
Форма введення пароля
Вкладка Information
Вкладка Main
Вкладка Security
Вкладка Boot
Вкладка Exit
Це успіх, панове.

Висновок

Драйвер написаний, код викладений на GitHub, залишилося перевірити ідею з OptionROM, і тема, можна сказати, закрита.
Якщо вам все ще незрозуміло, що тут взагалі відбувається, ви знайшли баг в коді, або просто хочете обговорити статтю, автора, монструозность UEFI або те, як добре було у часи legacy BIOS — ласкаво просимо в коментарі.
Дякую читачам за увагу, хороших вам DXE-драйверів.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.