В прошлой статье про 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-драйверов.

Комментарии (25)


  1. Lirein
    04.01.2016 10:35
    +3

    Большое спасибо за статью, думаю пригодится при разработке драйверов EFI для встраиваемых систем.
    Скажите, efi драйвер для IA32, Intel64, ARM, MIPS на выходе получается одинаковым или есть платформенные зависимости? По идее код EFI должен быть байт-интерпретируемым.


    1. CodeRush
      04.01.2016 10:48
      +1

      Большое пожалуйста.
      Драйвер собирается для целевой архитектуры и для разных архитектур нужны разные драйверы. По идее, есть возможность собрать DXE-драйвер в байткод EBC, но, во-первых, далеко не во всех прошивках есть драйвер для исполнения этого байткода (зовут его EbcDxe, можете открыть свою прошивку в UEFITool и поискать такой), во-вторых, сам этот исполнитель пока собирается только для IA32, AMD64 и IA-64, т.е. ARM, MIPS и PPC все равно в пролете, в-третьих, компилятор в EBC стоит денег, а бесплатно можно только писать непосредственно на байткоде, как это делает icbook.
      В общем, намного проще собрать несколько платформо-зависимых драйверов и рядом их положить, места сейчас для OptionROM хватает, а для обычной прошивки кроссплатформенность вообще не нужна, если процессор вдруг не поменятся с x86 на ARM (у AMD были планы сделать попиново совместимые процессоры с различными ядрами, но от них отказались, и правильно, на мой взгляд).


      1. Lirein
        04.01.2016 10:55
        +1

        Понятно, значит для каждого варианта аппаратной платформы проще держать отдельную версию UEFI BIOS, а при отсутствии поддержки — uboot. Хотя при наличии практики, с EFI работать значительно проще, особенно на стадии отладки устройства.
        Как тогда работают вот эти драйверы: efi.akeo.ie?


        1. CodeRush
          04.01.2016 11:01

          Придется держать отдельные версии в любом случае — исполнитель EBC стартует в начале фазы DXE, а там еще до этого две фазы на полмегабайта кода суммарно, которые делают всю черную работу по инициализации процессора, кэша L2, оперативной памяти и других жезненно необходимых устройств. Это все на EBC не напишешь, как не крутись.
          Драйверы те написаны на тулките GNU-EFI и тоже собираются по экземпляру для каждой архитектуры, на данный момент только для x86 и amd64.


      1. icbook
        04.01.2016 13:05

        Если в прошивке нет EbcDxe (что иногда встречается), то этот драйвер можно запустить командой run. Такое решение открывает путь для EBC, как универсального средства общения (с известными оговорками «универсального», кончено).


        1. CodeRush
          04.01.2016 13:33

          И тогда этих EbcDxe тоже надо с собой носить по количеству архитектур. Если основной драйвер огромный (UGA/GOP-драйвер для Radeon for MAC, к примеру), то есть смысл собрать его в EBC и носить с собой пригоршню интерпретаторов на каждый случай, а вот для таких поделок в 3 экрана EBC — только лишний оверхед.


          1. icbook
            04.01.2016 17:54
            +1

            Это правда. Наличие платформ, лишенных интерпретатора EBC, приводит к нивелированию идеи кроссплатформенности.
            UPD. Sorry, не run, а load.


  1. icbook
    04.01.2016 13:09
    +1

    Спасибо за статью! Давно пора было бы суммировать наработки в этом направлении: стоит вспомнить хотя бы рекомендации Курта Цзяо (Kurt Qiao). Непонятно, что мешает производителям разместить это вобщем-то тривиальную фичу в UEFI BIOS?


    1. CodeRush
      04.01.2016 13:25

      Что мешает — да ничего, собственно. ASUS, к примеру, давно уже позволяет сохранить по F12 скриншот BIOS Setup в формате BMP. Почему это до сих пор не внедрено в массы — черт его знает, разработчики не видят необходимости (у них перенаправление текстовой консоли есть, его хватает в 95% случаев), менеджеры никогда не работали с BIOS Setup и не знаю про него ничего, а пользователи не могу достучаться до людей, принимающих решения, через три линии «технической поддержки».


      1. icbook
        04.01.2016 13:44

        Ноутбучный UEFI у ASUS не очень-то и сохраняет скриншоты :) Я, конечно, понимаю, что через IPMI можно все что угодно задокументировать (да и сам так не раз делал), но вот на продакш-сервере не очень то и разгонишься перезагружаться с этой в общем-то благородной целью.
        Вот, кстати, хорошая иллюстрация, как скриншоты на серваке снимали удаленно :)


  1. Sergey6661313
    04.01.2016 13:09

    всегда удивлялся почему во всяких игрушках где скриншоты нужны только чтобы выпендрица, разработчики вставляют собственные перехватчики для снятия скриншотов, а в программах где действительно без скриншота бывает не обьясниш — нет.


    1. dprotopopov
      04.01.2016 13:15

      скриншот это для примера


    1. CodeRush
      04.01.2016 13:27

      Просто потому, что в игры играют миллионы, а настройками BIOS Setup делятся десятки. Денег на этой фиче не заработать, только из за нее плату у тебя, а не у конкурента, никто не купит — т.е. для производителя смысла в этой фиче почти нет. Мне она понадобилась — вот я ее и написал.


  1. dprotopopov
    04.01.2016 13:35

    скриншот это для примера
    ведь не только скришоты там делать — но и что-то полезное/бесполезное. Например встроенную защиту или вирус.


    1. CodeRush
      04.01.2016 13:39

      В данном случае, это все же больше для скриншотов, чем для примера.
      Интересущихся «вирусами» советую посмотреть код SMM Backdoor уважаемого d_olex, и его запись в блоге о разработке этого бэкдора.


      1. dprotopopov
        04.01.2016 14:00

        безопасность она базируется всегда на закрытом периметре/границе — если есть доступ к девайсу то всегда его вылечить можно


    1. icbook
      04.01.2016 13:45

      что-то типа тотал-коммандера нужно по зарез! и антивирусники, безусловно!


      1. dprotopopov
        04.01.2016 13:55

        да
        и truecrypt


  1. Kolyuchkin
    04.01.2016 16:33
    +1

    Спасибо большое за статью. Ваши статьи не только интересны, но и полезны с точки зрения простоты изложения материала. И можно все попробовать) Если будет время на выходных, то я постараюсь запустить Ваш драйвер из OpROM нашей железяки и выложить здесь скриншоты. Извините за оффтоп, но у меня есть вопрос по русификации вывода драйвера, если прошивка не поддерживает русский язык. Сложно ли это сделать? Достаточно ли реализовать протокол работы с шрифтами внутри драйвера?


    1. CodeRush
      04.01.2016 16:45

      Если получится запустить из OROM, будет совсем хорошо, спасибо за попытку заранее.
      По руссификации — не сталкивался с этой задачей, но думаю, что пары протоколов будет достаточно (знать бы только каких именно...). В крайнем случае, интерфейс можно весь в графической консоли битмапами рисовать, как в старые добрые времена.


  1. icbook
    04.01.2016 19:49

    Для чтения образа экрана используется функциональность GOP.BLT? Прямое обращение к видеопамяти исключено полностью?


    1. CodeRush
      04.01.2016 19:57

      Да, только GOP->Blt, можно было бы еще добавить поддержку протокола UGA ради макбуков, но у меня нет тестовой системы.


  1. icbook
    04.01.2016 21:01

    Тогда, по идее, это должно без проблем работать и на видеоадаптерах со статусом BLT Only.


  1. egyp7
    06.01.2016 11:43
    +1

    CodeRush спасибо. действительно хорошая статья для начинающих.
    у меня возник вопрос по вашей статье, он касается метода сохранения изображения на доступной файловой системе.
    выше вы пишете:

    Чаще всего единственные известные ФС — семейство FAT12/16/32 (иногда только FAT32), которые по стандарту UEFI могут использоваться для ESP.

    получается что на NTFS/Ext3/HFS писать ничего не получится? есть ли какие нибудь возможные варианты для решения сего вопроса?
    заранее благодарю за ответ.

    // upd.: посмотрел решение записи файлов в vector-edk от h-team. там используется модуль NtfsPkg, т.е вопрос с NTFS в данном случае решен. остается вопрос только с юниксовыми фс.


    1. CodeRush
      06.01.2016 12:19

      На здоровье. С юниксовыми ФС тоже больших проблем нет, драйверы для Ext2/3/4 и HfsPlus уже написаны. Проблем особых написание таких драйверов не составляет, чаще всего их можно из *nix портировать без особого бубна.