Пишем DXE-драйвер для снятия скриншотов с BIOS Setup и других UEFI-приложений

в 1:06, , рубрики: BIOS Setup, C, DXE-драйвер, open source, UEFI, костыли и подпорки, разработка, системное программирование, скриншоты

Пишем DXE-драйвер для снятия скриншотов с BIOS Setup и других UEFI-приложений - 1 В прошлой статье про SecureBoot мне очень не хватало возможности сделать снимок экрана при настройке UEFI через BIOS Setup, но тогда выручило перенаправление текстовой консоли в последовательный порт. Это отличное решение, но доступно оно на немногих серверных материнских платах, и через него можно получить только псевдографику, а хотелось бы получить настоящую — она и выглядит приятнее, и вырезать ее каждый раз из окна терминала не надо.
Вот именно этим мы и займемся в этой статье, а заодно я расскажу, что такое DXE-драйвер и как написать, собрать и протестировать такой самостоятельно, как работают ввод с клавиатуры и вывод на экран в UEFI, как найти среди подключенных устройств хранения такое, на которое можно записывать файлы, как сохранить что-нибудь в файл из UEFI и как адаптировать какой-то внешний код на С для работы в составе прошивки.
Если вам все еще интересно — жду вас под катом.

Отказ от ответственности

Прежде чем говорить о написании и отладке драйверов для UEFI, стоит сразу же сказать, что эксперименты с прошивкой — дело опасное, они могут привести к «кирпичу», а в самых неудачных редких случаях — к выходу из строя аппаратуры, поэтому я заранее предупреждаю: всё, что вы тут прочитаете, вы используете на свой страх и риск, я не несу и не буду нести ответственность за потерю работоспособности вашей прошивки или платы. Прежде чем начинать любые эксперименты с прошивкой, необходимо сделать полную копию всего содержимого SPI flash при помощи программатора. Только так вы можете гарантировать успешное восстановление прошивки после любого программного сбоя.
Если у вас нет программатора, но попробовать написать и отладить DXE-драйвер очень хочется, используйте для этого OVMF, VmWare Workstation 12 или любые другие системы виртуализации с поддержкой UEFI на ваш выбор.

Что там нужно и почему это DXE-драйвер

Задача наша состоит в том, чтобы снять скриншот со всего экрана во время работы какого-то UEFI-приложения, например BIOS Setup, нажатием определенной комбинации клавиш, найти файловую систему с доступом на запись и сохранить полученный скриншот на нее. Также было бы неплохо получить какую-то индикацию статуса. Т.к. для снятия скриншота потребуется прерывать работу UEFI-приложений, сама программа по их снятию приложением быть не может, ведь никакой вытесняющей многозадачности в UEFI пока еще не предусмотрено, поэтому нам нужен DXE-драйвер.
Схема его работы планируется примерно следующая:
0. Загружаемся только после появления текстового ввода (чтобы обрабатывать нажатия комбинации клавиш) и графического вывода (чтобы было с чего снимать скриншоты).
1. Вешаем обработчик нажатия комбинации LCtrl + LAlt + F12 (или любой другой на ваш вкус) на все доступные входные текстовые консоли.
2. В обработчике находим все выходные графические консоли, делаем с них скриншот и перекодируем его в формат PNG (т.к. UEFI-приложения обычно не используют миллионы цветов, то в этом формате скриншоты получаются размером в десятки килобайт вместо нескольких мегабайт в BMP).
3. В том же обработчике находим первую попавшуюся ФС с возможностью записи в корень и сохраняем туда полученные файлы.
Можно расширить функциональность выбором не первой попавшейся ФС, а, к примеру, только USB-устройств или только разделов ESP, оставим это на самостоятельную работу читателю.

Выбираем SDK

Для написания нового кода для работы в UEFI имеются два различных SDK — более новый EDK2 от UEFI Forum и GNU-EFI от независимых разработчиков, основанный на старом коде Intel. Оба решения подразумевают, что вы будете писать код на C и/или ассемблере, в нашем случае постараемся обойтись чистым C.
Не мне судить, какой SDK лучше, но я предлагаю использовать EDK2, т.к. он официальный и кроссплатформенный, и новые фичи (вместе с исправлением старых багов) появляются в нем значительно быстрее благодаря близости к источнику изменений, плюс именно его используют все известные мне IBV для написания своего кода.
EDK2 находится в процессе постоянной разработки, и в его trunk стабильно добавляют по 2-3 коммита в день, но так как мы здесь за самыми последними веяниями не гонимся (все равно они еще ни у кого не работают), поэтому будем использовать последний на данный момент стабильный срез EDK2, который называется UDK2015.
Чтобы обеспечить кроссплатформенность и возможность сборки различными компиляторами, EDK2 генерирует make-файлы для каждой платформы, используя конфигурационные файлы TXT (конфигурация окружения), DEC, DSC и FDF (конфигурация пакета) и INF (конфигурация компонента), подробнее о них я расскажу по ходу повествования, а сейчас нужно достать EDK2 и собрать HelloWorld, чем и займемся, если же вам не терпится узнать подробности прямо сейчас — проследуйте в документацию.

Настраиваем сборочное окружение

Подразумевается, что нужное для сборки кода на C и ассемблере ПО уже установлено на вашей машине. Если нет, пользователям Windows предлагаю установить Visual Studio 2013 Express for Windows Desktop, пользователям Linux и OSX понадобятся GCC 4.4-4.9 и NASM.
Если все это уже установлено, осталось только скачать UDK2015, распаковать все содержимое UDK2015.MyWorkSpace.zip туда, где у вас есть право на создание файлов (да хоть прямо на рабочий стол или в домашнюю директорию), а затем распаковать содержимое BaseTools(Windows).zip или BaseTools(Unix.zip) в получившуюся на предыдущем шаге директорию MyWorkSpace, которую затем переименовать в что-то приличное, например в UDK2015.
Теперь открываем терминал, переходим в только что созданную директорию UDK2015 и выполняем там скрипт edksetup.bat (или .sh), который скопирует в поддиректорию Conf набор текстовых файлов, нас будут интересовать tools_def.txt и target.txt.
Первый файл достаточно большой, в нем находятся определения переменных окружения с путями до необходимых сборочному окружению компиляторов C и ASL, ассемблеров, линковщиков и т.п. Если вам нужно, можете исправить указанные там пути или добавить свой набор утилит (т.н. ToolChain), но если вы послушали моего совета, то вам без изменений подойдет либо VS2013 (если у вас 32-разрядная Windows), либо VS2013x86 (в случае 64-разрядной Windows), либо GCC44 |… | GCC49 (в зависимости от вашей версии GCC, которую тот любезно показывает в ответ на gcc --version).
Во втором файле содержатся настройки сборки по умолчанию, в нем я рекомендую установить следующие значения:

ACTIVE_PLATFROM = MdeModulePkg/MdeModulePkg.dsc # Основной пакет для разработки модулей
TARGET = RELEASE  # Релизная конфигурация
TARGET_ARCH = X64 # DXE на большинстве современным машин 64-битная, исключения очень редки и очень болезненны
TOOL_CHAN_TAG = VS2013x86 # | VS2013 | GCC44 | ... | GCC49 | YOUR_FANCY_TOOLCHAIN, выберите наиболее подходящий в вашем случае

Откройте еще один терминал в UDK2015 и в Linux/OSX выполните команду:

. edksetup.sh BaseTools

В случае Windows достаточно обычного edksetup.bat без параметров.
Теперь протестируем сборочное окружение командой build, если все было сделано верно, то после определенного времени на закончится сообщением вроде

- Done -
Build end time: ...
Build total time: ...

Если же вместо Done вы видите Failed, значит с вашими настройками что-то не так. Я проверил вышеуказанное на VS2013x86 в Windows и GCC48 в Xubuntu 14.04.3 — УМВР.

Структура проекта

Приложения и драйверы в EDK2 собираются не отдельно, а в составе т.н Package, т.е. пакета. В пакет, кроме самих приложений, входят еще и библиотеки, наборы заголовочных файлов и файлы с описанием конфигурации пакета и его содержимого. Сделано это для того, чтобы позволить различным драйверам и приложениям использовать различные реализации библиотек, иметь доступ к различным заголовочным файлам и GUID'ам. Мы будем использовать MdeModulePkg, это очень общий пакет без каких-либо зависимостей от архитектуры и железа, и если наш драйвер удастся собрать в нем, он почти гарантированно будет работать на любых реализациях UEFI 2.1 и более новых. Минусом такого подхода является то, что большая часть библиотек в нем (к примеру, DebugLib, используемая для получения отладочного вывода) — просто заглушки, и их придется писать самому, если возникнет такая необходимость.
Для сборки нашего драйвера понадобится INF-файл с информацией о том, какие именно библиотеки, протоколы и файлы ему нужны для сборки, а также добавление пути до этого INF-файла в DSC-файл пакета, чтобы сборочная система вообще знала, что такой INF-файл есть.
Начнем с конца: открываем файл UDK2015/MdeModulePkg/MdeModulePkg.dsc и пролистываем его до раздела [Components] (можно найти его поиском — это быстрее). В разделе перечислены по порядку все файлы, принадлежащие пакету, выглядит начало раздела вот так:

[Components]
  MdeModulePkg/Application/HelloWorld/HelloWorld.inf
  MdeModulePkg/Application/MemoryProfileInfo/MemoryProfileInfo.inf
  ...

Добавляем туда свой будущий INF-файл вместе с путем до него относительно UDK2015. Предлагаю создать для него прямо в MdeModulePkg папку CrScreenshotDxe, а сам INF-файл назвать CrScreenshotDxe.inf. Как вы уже догадались, Cr — это от «CodeRush», а автор этой статьи — сама скромность. В результате получится что-то такое:

[Components]
  MdeModulePkg/CrScreenshotDxe/CrScreenshotDxe.inf
  MdeModulePkg/Application/HelloWorld/HelloWorld.inf
  MdeModulePkg/Application/MemoryProfileInfo/MemoryProfileInfo.inf
  ...

Сохраняем изменения и закрываем DSC-файл, больше мы его менять не будем, если не захотим настроить отладочный вывод, но это уже совсем другая история.
Теперь нужно заполнить сам INF-файл:

Выглядеть он будет примерно так

[Defines]                                               # Основные определения
  INF_VERSION    = 0x00010005                           # Версия спецификации, нам достаточно 1.5
  BASE_NAME      = CrScreenshotDxe                      # Название компонента
  FILE_GUID      = cab058df-e938-4f85-8978-1f7e6aabdb96 # GUID компонента
  MODULE_TYPE    = DXE_DRIVER                           # Тип компонента
  VERSION_STRING = 1.0                                  # Версия компонента
  ENTRY_POINT    = CrScreenshotDxeEntry                 # Имя точки входа

[Sources.common]                     # Файлы для сборки, common - общие для всех арзитектур 
  CrScreenshotDxe.c                  # Код нашего драйвера
  #...                               # Может быть, нам понадобится что-то еще, конвертер в PNG, к примеру

[Packages]                           # Используемые пакеты 
  MdePkg/MdePkg.dec                  # Основной пакет, без него не обходится ни один компонент UEFI
  MdeModulePkg/MdeModulePkg.dec      # Второй основной пакет, нужный драйверам и приложениям

[LibraryClasses]                     # Используемые библиотеки
  UefiBootServicesTableLib           # Удобный доступ к UEFI Boot Services через указатель gBS
  UefiRuntimeServicesTableLib        # Не менее удобный доступ к UEFI Runtime services через указатель gRT
  UefiDriverEntryPoint               # Точка входа в UEFI-драйвер, без нее конструкторы библиотек не сработают, а они нужны
  DebugLib                           # Для макроса DEBUG
  PrintLib                           # Для UnicodeSPrint, местного аналога snprintf

[Protocols]                          # Используемые протоколы
  gEfiGraphicsOutputProtocolGuid     # Доступ к графической консоли
  gEfiSimpleTextInputExProtocolGuid  # Доступ к текстовому вводу
  gEfiSimpleFileSystemProtocolGuid   # Доступ к файловым системам

[Depex]                              # Зависимости драйвера, пока эти протоколы недоступны, драйвер не запустится
  gEfiGraphicsOutputProtocolGuid AND # Доступ к ФС для запуска не обязателен, потом проверим его наличие в рантайме
  gEfiSimpleTextInputExProtocolGuid  # 

Осталось создать упомянутый выше файл CrScreenshotDxe.с:

С вот таким содержимым

#include <Uefi.h>
#include <Library/DebugLib.h>
#include <Library/PrintLib.h>
#include <Library/UefiDriverEntryPoint.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiRuntimeServicesTableLib.h>
#include <Protocol/GraphicsOutput.h>
#include <Protocol/SimpleTextInEx.h>
#include <Protocol/SimpleFileSystem.h>

EFI_STATUS
EFIAPI
CrScreenshotDxeEntry (
    IN EFI_HANDLE         ImageHandle,
    IN EFI_SYSTEM_TABLE   *SystemTable
    )
{
    return EFI_SUCCESS;
}

Если теперь повторить команду build, она должна быть успешной, иначе вы что-то сделали неправильно.
Вот теперь у нас, наконец, есть заготовка для нашего драйвера, и можно перейти непосредственно к написанию кода. Совершенно ясно, что такая сборочная система никуда не годится, и работать с ней через редактирование текстовых файлов не очень приятно, поэтому каждый из IBV имеет собственное решение по интеграции сборочной системы EDK2 в какую-нибудь современную IDE, к примеру среда AMI Visual eBIOS — это такой обвешенный плагинами Eclipse, а Phoenix и Insyde обвешивают ими же Visual Studio.
Есть еще замечательный проект VisualUefi за авторством известного специалиста по компьютерной безопасности Алекса Ионеску, и если вы тоже любите Visual Studio — предлагаю попробовать его, а мы пока продолжим угарать по хардкору, поддерживать дух старой школы и всё такое.

Реагируем на нажатие комбинации клавиш

Здесь все достаточно просто: при загрузке драйвера переберем все экземпляры протокола SimpleTextInputEx, который публикуется драйвером клавиатуры и чаще всего ровно один, даже в случае, когда к системе подключено несколько клавиатур — буфер то общий, если специально что-то не менять. Тем не менее, на всякий случай переберем все доступные экземпляры, вызвав у каждого функцию RegisterKeyNotify, которая
в качестве параметра принимает комбинацию клавиш, на которую мы намерены реагировать, и указатель на callback-функцию, которая будет вызвана после нажатия нужно комбинации, а в ней уже и будет проведена вся основная работа.

Переводим с русского на С

EFI_STATUS
EFIAPI
CrScreenshotDxeEntry (
    IN EFI_HANDLE         ImageHandle,
    IN EFI_SYSTEM_TABLE   *SystemTable
    )
{
    EFI_STATUS   Status;
    EFI_KEY_DATA KeyStroke;
    UINTN        HandleCount;
    EFI_HANDLE   *HandleBuffer = NULL;
    UINTN        i;
    
    // Set keystroke to be LCtrl+LAlt+F12
    KeyStroke.Key.ScanCode = SCAN_F12;
    KeyStroke.Key.UnicodeChar = 0;
    KeyStroke.KeyState.KeyShiftState = EFI_SHIFT_STATE_VALID | EFI_LEFT_CONTROL_PRESSED | EFI_LEFT_ALT_PRESSED;
    KeyStroke.KeyState.KeyToggleState = 0;
    
    // Locate all SimpleTextInEx protocols
    Status = gBS->LocateHandleBuffer(ByProtocol, &gEfiSimpleTextInputExProtocolGuid, NULL, &HandleCount, &HandleBuffer);
    if (EFI_ERROR (Status)) {
        DEBUG((-1, "CrScreenshotDxeEntry: gBS->LocateHandleBuffer returned %rn", Status));
        return EFI_UNSUPPORTED;
    }
    
    // For each instance
    for (i = 0; i < HandleCount; i++) {
        EFI_HANDLE Handle;
        EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *SimpleTextInEx;
        
        // Get protocol handle
        Status = gBS->HandleProtocol (HandleBuffer[i], &gEfiSimpleTextInputExProtocolGuid, (VOID **) &SimpleTextInEx);
        if (EFI_ERROR (Status)) {
           DEBUG((-1, "CrScreenshotDxeEntry: gBS->HandleProtocol[%d] returned %rn", i, Status));
           continue;
        }
        
        // Register key notification function
        Status = SimpleTextInEx->RegisterKeyNotify(
                SimpleTextInEx, 
                &KeyStroke, 
                TakeScreenshot, 
                &Handle);
        if (EFI_ERROR (Status)) {
            DEBUG((-1, "CrScreenshotDxeEntry: SimpleTextInEx->RegisterKeyNotify[%d] returned %rn", i, Status));
        }
    }
    
    // Free memory used for handle buffer
    if (HandleBuffer)
        gBS->FreePool(HandleBuffer);

    // Show driver loaded
    ShowStatus(0xFF, 0xFF, 0xFF); // White
    
    return EFI_SUCCESS;
}

Для успешной компиляции пока не хватает функций TakeScreenshot и ShowStatus, о которых ниже.

Ищем ФС с доступом на запись, пишем данные в файл

Прежде, чем искать доступные графические консоли и снимать с них скриншоты, нужно выяснить, можно ли эти самые скриншоты куда-то сохранить. Для этого нужно найти все экземпляры протокола SimpleFileSystem, который публикуется драйвером PartitionDxe для каждого обнаруженного тома, ФС которого известна прошивке. Чаще всего единственные известные ФС — семейство FAT12/16/32 (иногда только FAT32), которые по стандарту UEFI могут использоваться для ESP. Дальше нужно проверить, что на найденную ФС возможна запись, сделать это можно разными способами, самый простой — попытаться создать на ней файл и открыть его на чтение и запись, если получилось — на эту ФС можно писать. Решение, конечно, не самое оптимальное, но работающее, правильную реализацию предлагаю читателям в качестве упражнения.

Опять переводим с русского на С

EFI_STATUS
EFIAPI
FindWritableFs (
    OUT EFI_FILE_PROTOCOL **WritableFs
    )
{
    EFI_HANDLE *HandleBuffer = NULL;
    UINTN      HandleCount;
    UINTN      i;
    
    // Locate all the simple file system devices in the system
    EFI_STATUS Status = gBS->LocateHandleBuffer(ByProtocol, &gEfiSimpleFileSystemProtocolGuid, NULL, &HandleCount, &HandleBuffer);
    if (!EFI_ERROR (Status)) {
        EFI_FILE_PROTOCOL *Fs = NULL;
        // For each located volume
        for (i = 0; i < HandleCount; i++) {
            EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *SimpleFs = NULL;
            EFI_FILE_PROTOCOL *File = NULL;
            
            // Get protocol pointer for current volume
            Status = gBS->HandleProtocol(HandleBuffer[i], &gEfiSimpleFileSystemProtocolGuid, (VOID **) &SimpleFs);
            if (EFI_ERROR (Status)) {
                DEBUG((-1, "FindWritableFs: gBS->HandleProtocol[%d] returned %rn", i, Status));
                continue;
            }
            
            // Open the volume
            Status = SimpleFs->OpenVolume(SimpleFs, &Fs);
            if (EFI_ERROR (Status)) {
                DEBUG((-1, "FindWritableFs: SimpleFs->OpenVolume[%d] returned %rn", i, Status));
                continue;
            }
            
            // Try opening a file for writing
            Status = Fs->Open(Fs, &File, L"crsdtest.fil", EFI_FILE_MODE_CREATE | EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE, 0);
            if (EFI_ERROR (Status)) {
                DEBUG((-1, "FindWritableFs: Fs->Open[%d] returned %rn", i, Status));
                continue;
            }
            
            // Writable FS found
            Fs->Delete(File);
            *WritableFs = Fs;
            Status = EFI_SUCCESS;
            break;
        }
    }
    
    // Free memory
    if (HandleBuffer) {
        gBS->FreePool(HandleBuffer);
    }
    
    return Status;
}

Этому коду больше ничего не нужно, работает как есть.

Ищем графическую консоль и делаем снимок её экрана

Проверив, что сохранять скриншоты есть на что, займемся их снятием. Для этого понадобится перебрать все экземпляры протокола GOP, который публикуют GOP-драйверы и VideoBIOS'ы (точнее, не сам VBIOS, который ничего не знает ни про какие протоколы, а драйвер ConSplitter, реализующий прослойку между старыми VBIOS и UEFI) для каждого устройства вывода с графикой. У этого пртокола есть функция Blt для копирования изображения из фреймбуффера и в него, пока нам понадобится только первое. При помощи объекта Mode того же протокола можно получить текущее разрешение экрана, которое нужно для выделения буффера нужного размера и снятия скриншота со всего экрана, а не с какой-то его части. получив скриншот, стоит проверить что он не абсолютно черный, ибо сохранять такие — лишняя трата времени и места на ФС, черный прямоугольник нужного размера можно и в Paint нарисовать. Затем нужно преобразовать картинку из BGR (в котором её отдает Blt) в RGB (который нужен энкодеру PNG) иначе цвета на скриншотах будут неправильные. Кодируем полученную после конвертации картинку и сохраняем её в файл на той ФС, которую мы нашли на предыдущем шаге. Имя файла в формате 8.3 соберем из текущей даты и времени, так меньше шанс, что один скриншот перепишет другой.

Снова переводим с русского на С

EFI_STATUS
EFIAPI
TakeScreenshot (
    IN EFI_KEY_DATA *KeyData
    )
{
    EFI_FILE_PROTOCOL *Fs = NULL;
    EFI_FILE_PROTOCOL *File = NULL;
    EFI_GRAPHICS_OUTPUT_PROTOCOL  *GraphicsOutput = NULL;
    EFI_GRAPHICS_OUTPUT_BLT_PIXEL *Image = NULL;
    UINTN      ImageSize;         // Size in pixels
    UINT8      *PngFile = NULL;
    UINTN      PngFileSize;       // Size in bytes
    EFI_STATUS Status;
    UINTN      HandleCount;
    EFI_HANDLE *HandleBuffer = NULL;
    UINT32     ScreenWidth;
    UINT32     ScreenHeight;
    CHAR16     FileName[8+1+3+1]; // 0-terminated 8.3 file name
    EFI_TIME   Time;
    UINTN      i, j;

    // Find writable FS
    Status = FindWritableFs(&Fs);
    if (EFI_ERROR (Status)) {
        DEBUG((-1, "TakeScreenshot: Can't find writable FSn"));
        ShowStatus(0xFF, 0xFF, 0x00); // Yellow
        return EFI_SUCCESS;
    }
    
    // Locate all instances of GOP
    Status = gBS->LocateHandleBuffer(ByProtocol, &gEfiGraphicsOutputProtocolGuid, NULL, &HandleCount, &HandleBuffer);
    if (EFI_ERROR (Status)) {
        DEBUG((-1, "ShowStatus: Graphics output protocol not foundn"));
        return EFI_SUCCESS;
    }
    
    // For each GOP instance
    for (i = 0; i < HandleCount; i++) {
        do { // Break from do used instead of "goto error"
            // Handle protocol
            Status = gBS->HandleProtocol(HandleBuffer[i], &gEfiGraphicsOutputProtocolGuid, (VOID **) &GraphicsOutput);
            if (EFI_ERROR (Status)) {
                DEBUG((-1, "ShowStatus: gBS->HandleProtocol[%d] returned %rn", i, Status));
                break;
            }
        
            // Set screen width, height and image size in pixels
            ScreenWidth  = GraphicsOutput->Mode->Info->HorizontalResolution;
            ScreenHeight = GraphicsOutput->Mode->Info->VerticalResolution;
            ImageSize = ScreenWidth * ScreenHeight;
            
            // Get current time
            Status = gRT->GetTime(&Time, NULL);
            if (!EFI_ERROR(Status)) {
                // Set file name to current day and time
                UnicodeSPrint(FileName, 26, L"%02d%02d%02d%02d.png", Time.Day, Time.Hour, Time.Minute, Time.Second);
            }
            else {
                // Set file name to scrnshot.png
                UnicodeSPrint(FileName, 26, L"scrnshot.png");
            }
            
            // Allocate memory for screenshot
            Status = gBS->AllocatePool(EfiBootServicesData, ImageSize * sizeof(EFI_GRAPHICS_OUTPUT_BLT_PIXEL), (VOID **)&Image);
            if (EFI_ERROR(Status)) {
                DEBUG((-1, "TakeScreenshot: gBS->AllocatePool returned %rn", Status));
                break;
            }
        
            // Take screenshot
            Status = GraphicsOutput->Blt(GraphicsOutput, Image, EfiBltVideoToBltBuffer, 0, 0, 0, 0, ScreenWidth, ScreenHeight, 0);
            if (EFI_ERROR(Status)) {
                DEBUG((-1, "TakeScreenshot: GraphicsOutput->Blt returned %rn", Status));
                break;
            }
            
            // Check for pitch black image (it means we are using a wrong GOP)
            for (j = 0; j < ImageSize; j++) {
                if (Image[j].Red != 0x00 || Image[j].Green != 0x00 || Image[j].Blue != 0x00)
                    break;
            }
            if (j == ImageSize) {
                DEBUG((-1, "TakeScreenshot: GraphicsOutput->Blt returned pitch black image, skippedn"));
                ShowStatus(0x00, 0x00, 0xFF); // Blue
                break;
            }
            
            // Open or create output file
            Status = Fs->Open(Fs, &File, FileName, EFI_FILE_MODE_CREATE | EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE, 0);
            if (EFI_ERROR (Status)) {
                DEBUG((-1, "TakeScreenshot: Fs->Open of %s returned %rn", FileName, Status));
                break;
            }
            
            // Convert BGR to RGBA with Alpha set to 0xFF
            for (j = 0; j < ImageSize; j++) {
                UINT8 Temp = Image[j].Blue;
                Image[j].Blue = Image[j].Red;
                Image[j].Red = Temp;
                Image[j].Reserved = 0xFF;
            }
        
            // Encode raw RGB image to PNG format
            j = lodepng_encode32(&PngFile, &PngFileSize, (CONST UINT8*)Image, ScreenWidth, ScreenHeight);
            if (j) {
                DEBUG((-1, "TakeScreenshot: lodepng_encode32 returned %dn", j));
                break;
            }
                
            // Write PNG image into the file and close it
            Status = File->Write(File, &PngFileSize, PngFile);
            File->Close(File);
            if (EFI_ERROR(Status)) {
                DEBUG((-1, "TakeScreenshot: File->Write returned %rn", Status));
                break;
            }
            
            // Show success
            ShowStatus(0x00, 0xFF, 0x00); // Green
        } while(0);
        
        // Free memory
        if (Image)
            gBS->FreePool(Image);
        if (PngFile)
            gBS->FreePool(PngFile);
        Image = NULL;
        PngFile = NULL;
    }
    
    // Show error
    if (EFI_ERROR(Status))
        ShowStatus(0xFF, 0x00, 0x00); // Red
    
    return EFI_SUCCESS;
}

Для работы не хватает lodepng_encode32 и уже упоминавшейся выше ShowStatus, продолжим.

Кодируем изображение в формат PNG

Лучший способ писать код — не писать его, поэтому возьмем готовую библиотеку для кодирования и декодирования PNG по имени lodepng. Качаем, кладем рядом с нашим С-файлом, добавляем наш в INF-файл в раздел [Sources.common] строки lodepng.h и lodepng.c, включаем заголовочный файл, иии… ничего не компилируется, т.к lodepng не ожидает, что стандартная библиотека языка C может вот так вот брать и отсутствовать целиком. Ничего, допилим, не впервой.
В начало lodepng.h добавим следующее:

#include <Uefi.h>                           // Для успешной сборки в среде UEFI
#define LODEPNG_NO_COMPILE_DECODER          // Отключаем декодер PNG
#define LODEPNG_NO_COMPILE_DISK             // Отключаем запись на диск, т.к. fopen/fwrite у нас нет
#define LODEPNG_NO_COMPILE_ALLOCATORS       // Отключаем стандартные malloc/realloc/free, т.к. их у нас нет
#define LODEPNG_NO_COMPILE_ERROR_TEXT       // Отключаем сообщения об ошибках 
#define LODEPNG_NO_COMPILE_ANCILLARY_CHUNKS // Отключаем текстовые данные в PNG, т.к. не нужны
#if !defined(_MSC_VER)                      // Определяем тип size_t для GCC, у MS он встроен при настройках сборки по умолчанию
#define size_t UINTN
#endif

И закомментируем строку с #include <string.h>, которого у нас тоже нет. Можно, конечно, создать локальный файл с тем же именем, определив там тип size_t, но раз уж принялись менять — будем менять.
С lodepng.c немного сложнее, т.к. из стандартной библиотеки, кроме size_t, ему также нужны memset, memcpy, malloc, realloc, free, qsort, а еще он использует вычисления с плавающей точкой. Реализацию qsort можно утащить у Apple, функции работы с памятью сделать обертками над gBS->CopyMem, gBS->SetMem, gBS->AllocatePool и gBS->FreePool соответственно, а для того, чтобы сигнализировать о работе с FPU нужно определить константу CONST INT32 _fltused = 0;, иначе линковщик будет ругаться на её отсутствие. Про комментирование файлов со стандартными #include'ами я уже не говорю — все и так понятно.
Аналогичным образом к нормальному бою приводится и qsort.c, не забудьте только добавить его в INF-файл.

Выводим статус

Осталось написать функцию ShowStatus и наш драйвер готов. Получать этот самый статус можно разными способами, например, выводить числа от 0x00 до 0xFF в CPU IO-порт 80h, который подключен к POST-кодеру, но есть он далеко не у всех, а на ноутбуках — вообще не встречается. Можно пищать спикером, но это, во-первых, платформо-зависимо, а во-вторых — дико бесит уже после пары скриншотов. Можно мигать лампочками на клавиатуре, это дополнительное задание для читателя, а мы будем показывать статус работы с графической консолью прямо через эту графическую консоль — отображая маленький квадрат нужного цвета в левом верхнем углу экрана. При этом белый квадрат будет означать «драйвер успешно загружен», желтый — «ФС с возможностью записи не найдена», синий — «Скриншот текущей консоли полностью черный, сохранять нет смысла», красный — «произошла ошибка» и, наконец, зеленый — «скриншот снят и сохранен». Выводить это квадрат нужно на все консоли, а после короткого времени восстанавливать тот кусочек изображения, который им был затерт.

В последний раз переводим с русского на С

EFI_STATUS
EFIAPI
ShowStatus (
    IN UINT8 Red, 
    IN UINT8 Green, 
    IN UINT8 Blue
    )
{
    // Determines the size of status square
    #define STATUS_SQUARE_SIDE 5

    UINTN        HandleCount;
    EFI_HANDLE   *HandleBuffer = NULL;
    EFI_GRAPHICS_OUTPUT_PROTOCOL  *GraphicsOutput = NULL;
    EFI_GRAPHICS_OUTPUT_BLT_PIXEL Square[STATUS_SQUARE_SIDE * STATUS_SQUARE_SIDE];
    EFI_GRAPHICS_OUTPUT_BLT_PIXEL Backup[STATUS_SQUARE_SIDE * STATUS_SQUARE_SIDE];
    UINTN i;
    
    // Locate all instances of GOP
    EFI_STATUS Status = gBS->LocateHandleBuffer(ByProtocol, &gEfiGraphicsOutputProtocolGuid, NULL, &HandleCount, &HandleBuffer);
    if (EFI_ERROR (Status)) {
        DEBUG((-1, "ShowStatus: Graphics output protocol not foundn"));
        return EFI_UNSUPPORTED;
    }
    
    // Set square color
    for (i = 0 ; i < STATUS_SQUARE_SIDE * STATUS_SQUARE_SIDE; i++) {
        Square[i].Blue = Blue;
        Square[i].Green = Green;
        Square[i].Red = Red;
        Square[i].Reserved = 0x00;
    }
    
    // For each GOP instance
    for (i = 0; i < HandleCount; i ++) {
        // Handle protocol
        Status = gBS->HandleProtocol(HandleBuffer[i], &gEfiGraphicsOutputProtocolGuid, (VOID **) &GraphicsOutput);
        if (EFI_ERROR (Status)) {
            DEBUG((-1, "ShowStatus: gBS->HandleProtocol[%d] returned %rn", i, Status));
            continue;
        }
            
        // Backup current image
        GraphicsOutput->Blt(GraphicsOutput, Backup, EfiBltVideoToBltBuffer, 0, 0, 0, 0, STATUS_SQUARE_SIDE, STATUS_SQUARE_SIDE, 0);
        
        // Draw the status square
        GraphicsOutput->Blt(GraphicsOutput, Square, EfiBltBufferToVideo, 0, 0, 0, 0, STATUS_SQUARE_SIDE, STATUS_SQUARE_SIDE, 0);
        
        // Wait 500ms
        gBS->Stall(500*1000);
        
        // Restore the backup
        GraphicsOutput->Blt(GraphicsOutput, Backup, EfiBltBufferToVideo, 0, 0, 0, 0, STATUS_SQUARE_SIDE, STATUS_SQUARE_SIDE, 0);
    }
    
    return EFI_SUCCESS;
}

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

Тестируем результат в UEFI Shell

Забираем наш собранный драйвер из UDK2015/Build/MdeModulePkg/RELEASE/X64/MdeModulePkg/CrScreenshotDxe/CrScreenshotDxe/OUTPUT, понадобятся нам оттуда только два файла — сам драйвер CrScreenshotDxe.efi и секция зависимостей для него CrScreenshotDxe.depex
Для начала протестируем работу драйвера из UEFI Shell. Скопируйте файл CrScreenshotDxe.efi на USB-флешку с UEFI Shell, загрузитесь в него, перейдите в корень флешки командой fs0: (номер может меняться в зависимости от количества подключенных к вашей системе дисков) и выполните команду load CrScreenshotDxe.efi. Если увидели сообщение об успехе и промелькнувший в верхнем углу экрана белый квадрат — значит драйвер загружен и работает. У меня это выглядит вот так:

UEFI Shell

Пишем DXE-драйвер для снятия скриншотов с BIOS Setup и других UEFI-приложений - 2
Этот скриншот, как и все последующие, снят нашим драйвером, поэтому квадрата в углу на нем не видно.

Дальше смело жмите LCtrl + LAlt + F12 и наблюдайте за статусом. На моих системах с AMI графическая консоль одна, и потому я вижу промелькнувший зеленый квадрат и получаю один скриншот за одно нажатие комбинации. На моих системах с Phoenix и Insyde оказалось по две графические консоли, одна из которых пустая, поэтому я вижу сначала синий квадрат, а затем зеленый, скриншот при этом тоже только один. Результат тестирования из UEFI Shell на них выглядит так же, только разрешение там уже не 800х600, а 1366х768.
Ну вот, из шелла все работает и можно снимать скриншоты с UEFI-приложений, вот такие:

RU.efi

Пишем DXE-драйвер для снятия скриншотов с BIOS Setup и других UEFI-приложений - 3

Тестируем результат в модифицированной прошивке

К сожалению, скриншот с BIOS Setup таким образом не снять — драйвер загружается слишком поздно. Решений возможных тут два, первое — добавить наш драйвер вместе с секцией зависимостей в DXE-том прошивки при помощи UEFITool, второй — добавить его же к OptionROM какого-нибудь PCIe-устройства, тогда и модификация прошивки не понадобится. Второй способ я еще попытаюсь реализовать позже, когда получу нужную железку, а вот с первым проблем никаких нет. Вставляем, шьем, стартуем, втыкаем флешку, заходим в BIOS Setup, нажимаем LCtrl + LAlt + F12 — вуаля, видим синий и зеленый квадраты, все работает. Выглядит результат вот так:

Форма ввода пароля

Пишем DXE-драйвер для снятия скриншотов с BIOS Setup и других UEFI-приложений - 4
Вкладка Information

Пишем DXE-драйвер для снятия скриншотов с BIOS Setup и других UEFI-приложений - 5
Вкладка Main

Пишем DXE-драйвер для снятия скриншотов с BIOS Setup и других UEFI-приложений - 6
Вкладка Security

Пишем DXE-драйвер для снятия скриншотов с BIOS Setup и других UEFI-приложений - 7
Вкладка Boot

Пишем DXE-драйвер для снятия скриншотов с BIOS Setup и других UEFI-приложений - 8
Вкладка Exit

Пишем DXE-драйвер для снятия скриншотов с BIOS Setup и других UEFI-приложений - 9

Это успех, господа.

Заключение

Драйвер написан, код выложен на GitHub, осталось проверить идею с OptionROM, и тема, можно сказать, закрыта.
Если вам все еще непонятно, что тут вообще происходит, вы нашли баг в коде, или просто хотите обсудить статью, автора, монструозность UEFI или то, как хорошо было во времена legacy BIOS — добро пожаловать в комментарии.
Спасибо читателям за внимание, хороших вам DXE-драйверов.

Автор: CodeRush

Источник

Поделиться новостью

* - обязательные к заполнению поля