Пишем 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 protocol handle         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;         // 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 %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 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 %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 PNG format             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 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 %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-драйверов.

ссылка на оригинал статьи http://habrahabr.ru/post/274463/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *