В прошлой статье про 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. Если увидели сообщение об успехе и промелькнувший в верхнем углу экрана белый квадрат — значит драйвер загружен и работает. У меня это выглядит вот так:
Этот скриншот, как и все последующие, снят нашим драйвером, поэтому квадрата в углу на нем не видно.
Дальше смело жмите 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-драйверов.
ссылка на оригинал статьи http://habrahabr.ru/post/274463/
Добавить комментарий