В прошлой статье про 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).
Во втором файле содержатся настройки сборки по умолчанию, в нем я рекомендую установить следующие значения:
Откройте еще один терминал в UDK2015 и в Linux/OSX выполните команду:ACTIVE_PLATFROM = MdeModulePkg/MdeModulePkg.dsc # Основной пакет для разработки модулей TARGET = RELEASE # Релизная конфигурация TARGET_ARCH = X64 # DXE на большинстве современным машин 64-битная, исключения очень редки и очень болезненны TOOL_CHAN_TAG = VS2013x86 # | VS2013 | GCC44 | ... | GCC49 | YOUR_FANCY_TOOLCHAIN, выберите наиболее подходящий в вашем случае
В случае Windows достаточно обычного edksetup.bat без параметров.. edksetup.sh BaseTools
Теперь протестируем сборочное окружение командой build, если все было сделано верно, то после определенного времени на закончится сообщением вроде
Если же вместо Done вы видите Failed, значит с вашими настройками что-то не так. Я проверил вышеуказанное на VS2013x86 в Windows и GCC48 в Xubuntu 14.04.3 — УМВР.- Done - Build end time: ... Build total time: ...
Структура проекта
Приложения и драйверы в EDK2 собираются не отдельно, а в составе т.н Package, т.е. пакета. В пакет, кроме самих приложений, входят еще и библиотеки, наборы заголовочных файлов и файлы с описанием конфигурации пакета и его содержимого. Сделано это для того, чтобы позволить различным драйверам и приложениям использовать различные реализации библиотек, иметь доступ к различным заголовочным файлам и GUID'ам. Мы будем использовать MdeModulePkg, это очень общий пакет без каких-либо зависимостей от архитектуры и железа, и если наш драйвер удастся собрать в нем, он почти гарантированно будет работать на любых реализациях UEFI 2.1 и более новых. Минусом такого подхода является то, что большая часть библиотек в нем (к примеру, DebugLib, используемая для получения отладочного вывода) — просто заглушки, и их придется писать самому, если возникнет такая необходимость.
Для сборки нашего драйвера понадобится INF-файл с информацией о том, какие именно библиотеки, протоколы и файлы ему нужны для сборки, а также добавление пути до этого INF-файла в DSC-файл пакета, чтобы сборочная система вообще знала, что такой INF-файл есть.
Начнем с конца: открываем файл UDK2015/MdeModulePkg/MdeModulePkg.dsc и пролистываем его до раздела [Components] (можно найти его поиском — это быстрее). В разделе перечислены по порядку все файлы, принадлежащие пакету, выглядит начало раздела вот так:
Добавляем туда свой будущий INF-файл вместе с путем до него относительно UDK2015. Предлагаю создать для него прямо в MdeModulePkg папку CrScreenshotDxe, а сам INF-файл назвать CrScreenshotDxe.inf. Как вы уже догадались, Cr — это от «CodeRush», а автор этой статьи — сама скромность. В результате получится что-то такое:[Components] MdeModulePkg/Application/HelloWorld/HelloWorld.inf MdeModulePkg/Application/MemoryProfileInfo/MemoryProfileInfo.inf ...
Сохраняем изменения и закрываем DSC-файл, больше мы его менять не будем, если не захотим настроить отладочный вывод, но это уже совсем другая история.[Components] MdeModulePkg/CrScreenshotDxe/CrScreenshotDxe.inf MdeModulePkg/Application/HelloWorld/HelloWorld.inf MdeModulePkg/Application/MemoryProfileInfo/MemoryProfileInfo.inf ...
Теперь нужно заполнить сам INF-файл:
Выглядеть он будет примерно так
Осталось создать упомянутый выше файл CrScreenshotDxe.с:[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 #
С вот таким содержимым
Если теперь повторить команду build, она должна быть успешной, иначе вы что-то сделали неправильно.#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; }
Вот теперь у нас, наконец, есть заготовка для нашего драйвера, и можно перейти непосредственно к написанию кода. Совершенно ясно, что такая сборочная система никуда не годится, и работать с ней через редактирование текстовых файлов не очень приятно, поэтому каждый из IBV имеет собственное решение по интеграции сборочной системы EDK2 в какую-нибудь современную IDE, к примеру среда AMI Visual eBIOS — это такой обвешенный плагинами Eclipse, а Phoenix и Insyde обвешивают ими же Visual Studio.
Есть еще замечательный проект VisualUefi за авторством известного специалиста по компьютерной безопасности Алекса Ионеску, и если вы тоже любите Visual Studio — предлагаю попробовать его, а мы пока продолжим угарать по хардкору, поддерживать дух старой школы и всё так��е.
Реагируем на нажатие комбинации клавиш
Здесь все достаточно просто: при загрузке драйвера переберем все экземпляры протокола SimpleTextInputEx, который публикуется драйвером клавиатуры и чаще всего ровно один, даже в случае, когда к системе подключено несколько клавиатур — буфер то общий, если специально что-то не менять. Тем не менее, на всякий случай переберем все доступные экземпляры, вызвав у каждого функцию RegisterKeyNotify, которая в качестве параметра принимает комбинацию клавиш, на которую мы намерены реагировать, и указатель на callback-функцию, которая будет вызвана после нажатия нужно комбинации, а в ней уже и будет проведена вся основная работа.
Переводим с русского на С
Для успешной компиляции пока не хватает функций TakeScreenshot и ShowStatus, о которых ниже.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; }
Ищем ФС с доступом на запись, пишем данные в файл
Прежде, чем искать доступные графические консоли и снимать с них скриншоты, нужно выяснить, можно ли эти самые скриншоты куда-то сохранить. Для этого нужно найти все экземпляры протокола 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 соберем из текущей даты и времени, так меньше шанс, что один скриншот перепишет другой.
Снова переводим с русского на С
Для работы не хватает lodepng_encode32 и уже упоминавшейся выше ShowStatus, продолжим.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; }
Кодируем изображение в формат PNG
Лучший способ писать код — не писать его, поэтому возьмем готовую библиотеку для кодирования и декодирования PNG по имени lodepng. Качаем, кладем рядом с нашим С-файлом, добавляем наш в INF-файл в раздел [Sources.common] строки lodepng.h и lodepng.c, включаем заголовочный файл, иии… ничего не компилируется, т.к lodepng не ожидает, что стандартная библиотека языка C может вот так вот брать и отсутствовать целиком. Ничего, допилим, не впервой.
В начало lodepng.h добавим следующее:
И закомментируем строку с #include <string.h>, которого у нас тоже нет. Можно, конечно, создать локальный файл с тем же именем, определив там тип size_t, но раз уж принялись менять — будем менять.#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
С 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-кодеру, но есть он далеко не у всех, а на ноутбуках — вообще не встречается. Можно пищать спикером, но это, во-первых, платформо-зависимо, а во-вторых — дико бесит уже после пары скриншотов. Можно мигать лампочками на клавиатуре, это дополнительное задание для читателя, а мы будем показывать статус работы с графической консолью прямо через эту графическую консоль — отображая маленький квадрат нужного цвета в левом верхнем углу экрана. При этом белый квадрат будет означать «драйвер успешно загружен», желтый — «ФС с возможностью записи не найдена», синий — «Скриншот текущей консоли полностью черный, сохранять нет смысла», красный — «произошла ошибка» и, наконец, зеленый — «скриншот снят и сохранен». Выводить это квадрат нужно на все консоли, а после короткого времени восстанавливать тот кусочек изображения, который им был затерт.
В последний раз переводим с русского на С
Вот теперь все готово и успешно собирается, если нет — пилите, пока не соберется, либо скачайте мой готовый драйвер с GitHub и сравните с вашим, может быть я какие-то изменения банально забыл описать.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; }
Тестируем результат в 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-драйверов.
