В прошлой статье про 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 %rn", Status));
return EFI_UNSUPPORTED;
}
// For each instance
for (i = 0; i < HandleCount; i++) {
EFI_HANDLE Handle;
EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *SimpleTextInEx;
// Get protocol handle
Status = gBS->HandleProtocol (HandleBuffer[i], &gEfiSimpleTextInputExProtocolGuid, (VOID **) &SimpleTextInEx);
if (EFI_ERROR (Status)) {
DEBUG((-1, "CrScreenshotDxeEntry: gBS->HandleProtocol[%d] returned %rn", 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 %rn", 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;
// For each located 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 %rn", i, Status));
continue;
}
// Open the volume
Status = SimpleFs->OpenVolume(SimpleFs, &Fs);
if (EFI_ERROR (Status)) {
DEBUG((-1, "FindWritableFs: SimpleFs->OpenVolume[%d] returned %rn", 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 %rn", 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 FSn"));
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 foundn"));
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 %rn", 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 day and 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 %rn", 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 %rn", 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, skippedn"));
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 %rn", 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 PNG format
j = lodepng_encode32(&PngFile, &PngFileSize, (CONST UINT8*)Image, ScreenWidth, ScreenHeight);
if (j) {
DEBUG((-1, "TakeScreenshot: lodepng_encode32 returned %dn", 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 %rn", 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 foundn"));
return EFI_UNSUPPORTED;
}
// Set square color
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 %rn", 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. Если увидели сообщение об успехе и промелькнувший в верхнем углу экрана белый квадрат — значит драйвер загружен и работает. У меня это выглядит вот так:
Этот скриншот, как и все последующие, снят нашим драйвером, поэтому квадрата в углу на нем не видно.
Дальше смело жмите LCtrl + LAlt + F12 и наблюдайте за статусом. На моих системах с AMI графическая консоль одна, и потому я вижу промелькнувший зеленый квадрат и получаю один скриншот за одно нажатие комбинации. На моих системах с Phoenix и Insyde оказалось по две графические консоли, одна из которых пустая, поэтому я вижу сначала синий квадрат, а затем зеленый, скриншот при этом тоже только один. Результат тестирования из UEFI Shell на них выглядит так же, только разрешение там уже не 800х600, а 1366х768.
Ну вот, из шелла все работает и можно снимать скриншоты с UEFI-приложений, вот такие:
Тестируем результат в модифицированной прошивке
К сожалению, скриншот с BIOS Setup таким образом не снять — драйвер загружается слишком поздно. Решений возможных тут два, первое — добавить наш драйвер вместе с секцией зависимостей в DXE-том прошивки при помощи UEFITool, второй — добавить его же к OptionROM какого-нибудь PCIe-устройства, тогда и модификация прошивки не понадобится. Второй способ я еще попытаюсь реализовать позже, когда получу нужную железку, а вот с первым проблем никаких нет. Вставляем, шьем, стартуем, втыкаем флешку, заходим в BIOS Setup, нажимаем LCtrl + LAlt + F12 — вуаля, видим синий и зеленый квадраты, все работает. Выглядит результат вот так:
Это успех, господа.
Заключение
Драйвер написан, код выложен на GitHub, осталось проверить идею с OptionROM, и тема, можно сказать, закрыта.
Если вам все еще непонятно, что тут вообще происходит, вы нашли баг в коде, или просто хотите обсудить статью, автора, монструозность UEFI или то, как хорошо было во времена legacy BIOS — добро пожаловать в комментарии.
Спасибо читателям за внимание, хороших вам DXE-драйверов.
Автор: CodeRush