Подробный гайд как написать DXE-драйвер для UEFI, который по горячей клавише снимает framebuffer

Гайд по созданию DXE-драйвера UEFI: захват экрана BIOS Setup через GOP, горячая клавиша и сохранение скриншота в BMP на FAT-носитель.

2026.08.21                  


Подробный гайд как написать DXE-драйвер для UEFI, который по горячей клавише снимает framebufferПодробный гайд как написать DXE-драйвер для UEFI, который по горячей клавише снимает framebuffer Гайд, как написать DXE-драйвер для UEFI, который по горячей клавише снимает framebuffer и сохраняет скриншот BIOS/UEFI Setup в BMP-файл на FAT-носитель.

Важно:

Делайте это только на своей системе или с письменного разрешения владельца. Драйвер работает до ExitBootServices и предназначен для отладки, тестирования Setup, локализации, автоматизации QA и т.п.


1. Общая идея

В UEFI Setup обычно рисует интерфейс через GOP — EFI_GRAPHICS_OUTPUT_PROTOCOL.



Поэтому самый переносимый способ сделать скриншот:

  1. Дождаться появления клавиатурного протокола EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL.
  2. Зарегистрировать горячую клавишу, например F8.
  3. По нажатию клавиши установить флаг.
  4. По таймеру проверить флаг и выполнить захват экрана.
  5. Через GOP->Blt() скопировать framebuffer в EFI_GRAPHICS_OUTPUT_BLT_PIXEL.
  6. Сохранить буфер как BMP через EFI_SIMPLE_FILE_SYSTEM_PROTOCOL.

Драйвер будет DXE-драйвером с ENTRY_POINT, без привязки к конкретному железу. Он использует стандартные boot services UEFI.


2. Что понадобится

1. EDK2.

2. Инструментальная цепочка:

  • Linux: GCC5, nasm, python3, uuid-dev, iasl.
  • Windows: Visual Studio и EDK2 build environment.

3. Платформа для сборки и проверки:

  • проще всего OVMF/QEMU;
  • для реального железа — ваша EDK2-платформа или vendor BIOS SDK.

4. FAT32-носитель для скриншотов:

  • USB-флешка;
  • ESP-раздел;
  • виртуальный FAT-диск в QEMU.

3. Структура файлов

Создайте в корне EDK2, например:

edk2/
  ScreenshotPkg/
    ScreenshotDxe/
      ScreenshotDxe.inf
      ScreenshotDxe.c

4. INF-файл драйвера

Файл:

[Defines]
  INF_VERSION                    = 0x00010019
  BASE_NAME                      = ScreenshotDxe
  FILE_GUID                      = 8F0D2C9A-7E6C-4D1B-9A3B-2D6D6D5B3E11
  MODULE_TYPE                    = DXE_DRIVER
  VERSION_STRING                 = 1.0
  ENTRY_POINT                    = ScreenshotDxeEntry

[Sources]
  ScreenshotDxe.c

[Packages]
  MdePkg/MdePkg.dec

[LibraryClasses]
  UefiDriverEntryPoint
  UefiBootServicesTableLib
  MemoryAllocationLib
  BaseMemoryLib
  UefiLib
  DebugLib
  PrintLib

[Protocols]
  gEfiGraphicsOutputProtocolGuid
  gEfiSimpleFileSystemProtocolGuid
  gEfiSimpleTextInputExProtocolGuid

[Depex]
  TRUE

Замените FILE_GUID на уникальный GUID.

В Linux можно использовать:

uuidgen

5. Исходный код драйвера

Ниже — рабочий учебный вариант. Он сохраняет BMP-файлы в каталог \Screenshots на первом найденном доступном для записи FAT-томе.

Файл ScreenshotDxe.c:

#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/DebugLib.h>
#include <Library/PrintLib.h>
#include <Protocol/GraphicsOutput.h>
#include <Protocol/SimpleFileSystem.h>
#include <Protocol/SimpleTextInputEx.h>
#include <Protocol/SimpleTextIn.h>

#define SCREENSHOT_DIR           L"\\Screenshots"
#define MAX_REGISTERED_INPUTS    16
#define CAPTURE_TIMER_PERIOD     1000000ULL  // 100 ms, в единицах 100 ns
#define SCREENSHOT_SCAN_CODE     SCAN_F8     // Замените на удобную клавишу

#pragma pack(push, 1)
typedef struct {
  UINT16 bfType;
  UINT32 bfSize;
  UINT16 bfReserved1;
  UINT16 bfReserved2;
  UINT32 bfOffBits;
} BMP_FILE_HEADER;

typedef struct {
  UINT32 biSize;
  INT32  biWidth;
  INT32  biHeight;
  UINT16 biPlanes;
  UINT16 biBitCount;
  UINT32 biCompression;
  UINT32 biSizeImage;
  INT32  biXPelsPerMeter;
  INT32  biYPelsPerMeter;
  UINT32 biClrUsed;
  UINT32 biClrImportant;
} BMP_INFO_HEADER;
#pragma pack(pop)

STATIC EFI_EVENT  mInputProtocolEvent = NULL;
STATIC VOID       *mInputProtocolRegistration = NULL;
STATIC EFI_EVENT  mTimerEvent = NULL;

STATIC EFI_HANDLE mRegisteredInputs[MAX_REGISTERED_INPUTS];
STATIC UINTN      mRegisteredInputCount = 0;

STATIC volatile BOOLEAN mCaptureRequested = FALSE;
STATIC UINT32           mShotIndex = 0;

STATIC BOOLEAN IsInputHandleRegistered (EFI_HANDLE Handle);
STATIC EFI_STATUS EFIAPI ScreenshotKeyNotify (EFI_KEY_DATA *KeyData);
STATIC VOID RegisterInputNotifications (VOID);
STATIC VOID EFIAPI InputProtocolNotify (EFI_EVENT Event, VOID *Context);
STATIC VOID EFIAPI TimerNotify (EFI_EVENT Event, VOID *Context);
STATIC VOID ConnectAllControllers (VOID);
STATIC EFI_STATUS FindWritableRoot (EFI_FILE_PROTOCOL **Root);
STATIC EFI_STATUS WriteExactly (EFI_FILE_PROTOCOL *File, VOID *Buffer, UINTN Size);
STATIC EFI_STATUS WriteBmpFile (
  EFI_FILE_PROTOCOL             *File,
  EFI_GRAPHICS_OUTPUT_BLT_PIXEL *Blt,
  UINT32                        Width,
  UINT32                        Height
  );
STATIC EFI_STATUS SaveGopScreen (EFI_GRAPHICS_OUTPUT_PROTOCOL *Gop);
STATIC VOID CaptureAllScreens (VOID);

STATIC
BOOLEAN
IsInputHandleRegistered (
  EFI_HANDLE Handle
  )
{
  UINTN Index;

  for (Index = 0; Index < mRegisteredInputCount; Index++) {
    if (mRegisteredInputs[Index] == Handle) {
      return TRUE;
    }
  }

  return FALSE;
}

STATIC
EFI_STATUS
EFIAPI
ScreenshotKeyNotify (
  IN EFI_KEY_DATA *KeyData
  )
{
  if ((KeyData != NULL) && (KeyData->Key.ScanCode == SCREENSHOT_SCAN_CODE)) {
    //
    // Не делаем файловый ввод-вывод прямо из key notify.
    // Только выставляем флаг, а таймер выполнит основную работу.
    //
    mCaptureRequested = TRUE;
  }

  return EFI_SUCCESS;
}

STATIC
VOID
RegisterInputNotifications (
  VOID
  )
{
  EFI_STATUS                          Status;
  EFI_HANDLE                          *HandleBuffer;
  UINTN                               HandleCount;
  UINTN                               Index;
  EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL   *InputEx;
  EFI_KEY_DATA                        KeyData;
  EFI_HANDLE                          NotifyHandle;

  Status = gBS->LocateHandleBuffer (
                  ByProtocol,
                  &gEfiSimpleTextInputExProtocolGuid,
                  NULL,
                  &HandleCount,
                  &HandleBuffer
                  );
  if (EFI_ERROR (Status)) {
    return;
  }

  for (Index = 0; Index < HandleCount; Index++) {
    if (IsInputHandleRegistered (HandleBuffer[Index])) {
      continue;
    }

    if (mRegisteredInputCount >= MAX_REGISTERED_INPUTS) {
      break;
    }

    Status = gBS->HandleProtocol (
                    HandleBuffer[Index],
                    &gEfiSimpleTextInputExProtocolGuid,
                    (VOID **)&InputEx
                    );
    if (EFI_ERROR (Status)) {
      continue;
    }

    ZeroMem (&KeyData, sizeof (KeyData));
    KeyData.Key.ScanCode      = SCREENSHOT_SCAN_CODE;
    KeyData.Key.UnicodeChar   = L'\0';

    Status = InputEx->RegisterKeyNotify (
                        InputEx,
                        &KeyData,
                        ScreenshotKeyNotify,
                        &NotifyHandle
                        );
    if (!EFI_ERROR (Status)) {
      mRegisteredInputs[mRegisteredInputCount++] = HandleBuffer[Index];
      DEBUG ((DEBUG_INFO, "ScreenshotDxe: hotkey registered\n"));
    }
  }

  FreePool (HandleBuffer);
}

STATIC
VOID
EFIAPI
InputProtocolNotify (
  IN EFI_EVENT Event,
  IN VOID      *Context
  )
{
  RegisterInputNotifications ();
}

STATIC
VOID
EFIAPI
TimerNotify (
  IN EFI_EVENT Event,
  IN VOID      *Context
  )
{
  if (mCaptureRequested) {
    mCaptureRequested = FALSE;
    CaptureAllScreens ();
  }
}

STATIC
VOID
ConnectAllControllers (
  VOID
  )
{
  EFI_STATUS  Status;
  EFI_HANDLE  *HandleBuffer;
  UINTN       HandleCount;
  UINTN       Index;

  Status = gBS->LocateHandleBuffer (
                  AllHandles,
                  NULL,
                  NULL,
                  &HandleCount,
                  &HandleBuffer
                  );
  if (EFI_ERROR (Status)) {
    return;
  }

  for (Index = 0; Index < HandleCount; Index++) {
    gBS->ConnectController (
           HandleBuffer[Index],
           NULL,
           NULL,
           TRUE
           );
  }

  FreePool (HandleBuffer);
}

STATIC
EFI_STATUS
FindWritableRoot (
  OUT EFI_FILE_PROTOCOL **Root
  )
{
  EFI_STATUS                       Status;
  EFI_HANDLE                       *HandleBuffer;
  UINTN                            HandleCount;
  UINTN                            Index;
  EFI_SIMPLE_FILE_SYSTEM_PROTOCOL  *Fs;
  EFI_FILE_PROTOCOL                *RootHandle;
  EFI_FILE_PROTOCOL                *DirHandle;

  *Root = NULL;

  Status = gBS->LocateHandleBuffer (
                  ByProtocol,
                  &gEfiSimpleFileSystemProtocolGuid,
                  NULL,
                  &HandleCount,
                  &HandleBuffer
                  );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  for (Index = 0; Index < HandleCount; Index++) {
    Status = gBS->HandleProtocol (
                    HandleBuffer[Index],
                    &gEfiSimpleFileSystemProtocolGuid,
                    (VOID **)&Fs
                    );
    if (EFI_ERROR (Status)) {
      continue;
    }

    Status = Fs->OpenVolume (Fs, &RootHandle);
    if (EFI_ERROR (Status)) {
      continue;
    }

    //
    // Пытаемся создать каталог для скриншотов.
    // Если получилось — том подходит для записи.
    //
    DirHandle = NULL;
    Status = RootHandle->Open (
                           RootHandle,
                           &DirHandle,
                           SCREENSHOT_DIR,
                           EFI_FILE_MODE_CREATE | EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE,
                           EFI_FILE_DIRECTORY
                           );
    if (!EFI_ERROR (Status)) {
      DirHandle->Close (DirHandle);
      *Root = RootHandle;
      FreePool (HandleBuffer);
      return EFI_SUCCESS;
    }

    RootHandle->Close (RootHandle);
  }

  FreePool (HandleBuffer);
  return EFI_NOT_FOUND;
}

STATIC
EFI_STATUS
WriteExactly (
  IN EFI_FILE_PROTOCOL *File,
  IN VOID              *Buffer,
  IN UINTN             Size
  )
{
  EFI_STATUS  Status;
  UINTN       WriteSize;

  WriteSize = Size;
  Status = File->Write (File, &WriteSize, Buffer);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  return (WriteSize == Size) ? EFI_SUCCESS : EFI_DEVICE_ERROR;
}

STATIC
EFI_STATUS
WriteBmpFile (
  IN EFI_FILE_PROTOCOL             *File,
  IN EFI_GRAPHICS_OUTPUT_BLT_PIXEL *Blt,
  IN UINT32                        Width,
  IN UINT32                        Height
  )
{
  EFI_STATUS                     Status;
  BMP_FILE_HEADER                FileHeader;
  BMP_INFO_HEADER                InfoHeader;
  UINTN                          RowBytes;
  UINTN                          ImageSize;
  EFI_GRAPHICS_OUTPUT_BLT_PIXEL  *Row;
  INTN                           y;
  UINT32                         x;

  RowBytes  = (UINTN)Width * sizeof (EFI_GRAPHICS_OUTPUT_BLT_PIXEL);
  ImageSize = RowBytes * Height;

  ZeroMem (&FileHeader, sizeof (FileHeader));
  FileHeader.bfType     = 0x4D42; // 'BM'
  FileHeader.bfSize     = (UINT32)(sizeof (BMP_FILE_HEADER) + sizeof (BMP_INFO_HEADER) + ImageSize);
  FileHeader.bfOffBits  = sizeof (BMP_FILE_HEADER) + sizeof (BMP_INFO_HEADER);

  ZeroMem (&InfoHeader, sizeof (InfoHeader));
  InfoHeader.biSize          = sizeof (BMP_INFO_HEADER);
  InfoHeader.biWidth         = (INT32)Width;
  InfoHeader.biHeight        = (INT32)Height;
  InfoHeader.biPlanes        = 1;
  InfoHeader.biBitCount      = 32;
  InfoHeader.biCompression   = 0; // BI_RGB
  InfoHeader.biSizeImage     = (UINT32)ImageSize;
  InfoHeader.biXPelsPerMeter = 2835;
  InfoHeader.biYPelsPerMeter = 2835;

  Status = WriteExactly (File, &FileHeader, sizeof (FileHeader));
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Status = WriteExactly (File, &InfoHeader, sizeof (InfoHeader));
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Row = AllocatePool (RowBytes);
  if (Row == NULL) {
    return EFI_OUT_OF_RESOURCES;
  }

  //
  // BMP хранит строки снизу вверх.
  // GOP BLT buffer хранит строки сверху вниз.
  //
  for (y = (INTN)Height - 1; y >= 0; y--) {
    CopyMem (Row, Blt + ((UINTN)y * Width), RowBytes);

    //
    // EFI_GRAPHICS_OUTPUT_BLT_PIXEL имеет B, G, R, Reserved.
    // Для 32-бит BMP лучше сделать reserved/alpha непрозрачным.
    //
    for (x = 0; x < Width; x++) {
      Row[x].Reserved = 0xFF;
    }

    Status = WriteExactly (File, Row, RowBytes);
    if (EFI_ERROR (Status)) {
      break;
    }
  }

  FreePool (Row);
  return Status;
}

STATIC
EFI_STATUS
SaveGopScreen (
  IN EFI_GRAPHICS_OUTPUT_PROTOCOL *Gop
  )
{
  EFI_STATUS                        Status;
  EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *Info;
  UINTN                             SizeOfInfo;
  BOOLEAN                           FreeInfo;
  UINT32                            Width;
  UINT32                            Height;
  UINTN                             BufferSize;
  EFI_GRAPHICS_OUTPUT_BLT_PIXEL     *Blt;
  EFI_FILE_PROTOCOL                 *Root;
  EFI_FILE_PROTOCOL                 *File;
  CHAR16                            FileName[64];
  UINT32                            Index;

  Blt = NULL;

  if ((Gop == NULL) || (Gop->Mode == NULL)) {
    return EFI_UNSUPPORTED;
  }

  Info = Gop->Mode->Info;
  FreeInfo = FALSE;

  if ((Info == NULL) ||
      (Info->HorizontalResolution == 0) ||
      (Info->VerticalResolution == 0)) {
    Status = Gop->QueryMode (
                    Gop,
                    Gop->Mode->Mode,
                    &SizeOfInfo,
                    &Info
                    );
    if (EFI_ERROR (Status)) {
      return Status;
    }

    FreeInfo = TRUE;
  }

  Width  = Info->HorizontalResolution;
  Height = Info->VerticalResolution;

  if ((Width == 0) || (Height == 0)) {
    Status = EFI_UNSUPPORTED;
    goto Done;
  }

  BufferSize = (UINTN)Width * Height * sizeof (EFI_GRAPHICS_OUTPUT_BLT_PIXEL);
  Blt = AllocatePool (BufferSize);
  if (Blt == NULL) {
    Status = EFI_OUT_OF_RESOURCES;
    goto Done;
  }

  Status = Gop->Blt (
                  Gop,
                  Blt,
                  EfiBltVideoToBltBuffer,
                  0,
                  0,
                  0,
                  0,
                  Width,
                  Height,
                  0
                  );
  if (EFI_ERROR (Status)) {
    goto Done;
  }

  Status = FindWritableRoot (&Root);
  if (EFI_ERROR (Status)) {
    //
    // Возможно, файловая система еще не подключена.
    // Это тяжелая операция, но для отладки обычно допустимо.
    //
    ConnectAllControllers ();
    Status = FindWritableRoot (&Root);
  }

  if (EFI_ERROR (Status)) {
    goto Done;
  }

  Index = mShotIndex;

  UnicodeSPrint (
    FileName,
    sizeof (FileName),
    L"%s\\shot_%08d.bmp",
    SCREENSHOT_DIR,
    Index
    );

  Status = Root->Open (
                   Root,
                   &File,
                   FileName,
                   EFI_FILE_MODE_CREATE | EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE,
                   0
                   );
  if (!EFI_ERROR (Status)) {
    Status = WriteBmpFile (File, Blt, Width, Height);
    File->Flush (File);
    File->Close (File);

    if (!EFI_ERROR (Status)) {
      mShotIndex = Index + 1;
    }
  }

  Root->Close (Root);

Done:
  if (Blt != NULL) {
    FreePool (Blt);
  }

  if (FreeInfo && (Info != NULL)) {
    FreePool (Info);
  }

  return Status;
}

STATIC
VOID
CaptureAllScreens (
  VOID
  )
{
  EFI_STATUS                     Status;
  EFI_HANDLE                     *HandleBuffer;
  UINTN                          HandleCount;
  UINTN                          Index;
  EFI_GRAPHICS_OUTPUT_PROTOCOL   *Gop;

  HandleBuffer = NULL;
  HandleCount  = 0;

  Status = gBS->LocateHandleBuffer (
                  ByProtocol,
                  &gEfiGraphicsOutputProtocolGuid,
                  NULL,
                  &HandleCount,
                  &HandleBuffer
                  );
  if (EFI_ERROR (Status) || (HandleCount == 0)) {
    if (HandleBuffer != NULL) {
      FreePool (HandleBuffer);
    }

    ConnectAllControllers ();

    HandleBuffer = NULL;
    HandleCount  = 0;

    Status = gBS->LocateHandleBuffer (
                    ByProtocol,
                    &gEfiGraphicsOutputProtocolGuid,
                    NULL,
                    &HandleCount,
                    &HandleBuffer
                    );
    if (EFI_ERROR (Status) || (HandleCount == 0)) {
      if (HandleBuffer != NULL) {
        FreePool (HandleBuffer);
      }

      return;
    }
  }

  for (Index = 0; Index < HandleCount; Index++) {
    Status = gBS->HandleProtocol (
                    HandleBuffer[Index],
                    &gEfiGraphicsOutputProtocolGuid,
                    (VOID **)&Gop
                    );
    if (!EFI_ERROR (Status)) {
      SaveGopScreen (Gop);
    }
  }

  FreePool (HandleBuffer);
}

EFI_STATUS
EFIAPI
ScreenshotDxeEntry (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_STATUS  Status;

  //
  // Ждем появления Simple Text Input Ex, чтобы зарегистрировать горячую клавишу.
  //
  Status = gBS->CreateEvent (
                  EVT_NOTIFY_SIGNAL,
                  TPL_CALLBACK,
                  InputProtocolNotify,
                  NULL,
                  &mInputProtocolEvent
                  );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Status = gBS->RegisterProtocolNotify (
                  &gEfiSimpleTextInputExProtocolGuid,
                  mInputProtocolEvent,
                  &mInputProtocolRegistration
                  );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  //
  // Таймер нужен, чтобы не выполнять файловые операции из key callback.
  //
  Status = gBS->CreateEvent (
                  EVT_TIMER | EVT_NOTIFY_SIGNAL,
                  TPL_CALLBACK,
                  TimerNotify,
                  NULL,
                  &mTimerEvent
                  );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Status = gBS->SetTimer (
                  mTimerEvent,
                  TimerPeriodic,
                  CAPTURE_TIMER_PERIOD
                  );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  //
  // Если клавиатура уже есть — регистрируем hotkey сразу.
  //
  RegisterInputNotifications ();

  return EFI_SUCCESS;
}



6. Как это работает



6.1. Регистрация горячей клавиши



Драйвер подписывается на появление протокола:



gEfiSimpleTextInputExProtocolGuid


Когда появляется клавиатурное устройство с EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL, драйвер регистрирует callback на SCAN_F8.

Если хотите другую клавишу, измените:

#define SCREENSHOT_SCAN_CODE SCAN_F8

Например:

SCAN_F1
SCAN_F2
...
SCAN_F12
SCAN_ESC
SCAN_PAUSE

Но выбирайте клавишу, которую BIOS Setup не использует для важных действий.


6.2. Почему не сохраняем файл прямо из key callback

Key notify callback может вызываться в контексте, где нежелательны долгие операции.

Поэтому мы только ставим флаг:

mCaptureRequested = TRUE;

А таймер каждые 100 мс проверяет флаг и уже спокойно выполняет:

  • поиск GOP;
  • копирование framebuffer;
  • поиск файловой системы;
  • запись BMP.

6.3. Захват экрана

Захват делается через:

Gop->Blt (
       Gop,
       Blt,
       EfiBltVideoToBltBuffer,
       0,
       0,
       0,
       0,
       Width,
       Height,
       0
       );

Это стандартный способ получить текущий framebuffer в формате EFI_GRAPHICS_OUTPUT_BLT_PIXEL, независимо от конкретного формата пикселей видеокарты.


6.4. Сохранение BMP

Каждый пиксель GOP BLT имеет структуру:

typedef struct {
  UINT8 Blue;
  UINT8 Green;
  UINT8 Red;
  UINT8 Reserved;
} EFI_GRAPHICS_OUTPUT_BLT_PIXEL;

Для 32-битного BMP это подходит. Важно только:

  • BMP хранит строки снизу вверх;
  • reserved/alpha лучше заполнить 0xFF.

Код выше это делает.


7. Подключение драйвера к платформе

Есть два варианта:

  1. Встроить драйвер в прошивку.
  2. Загрузить драйвер вручную из UEFI Shell.

Для гарантированного попадания в BIOS Setup лучше встраивать в FV. Для быстрой проверки можно грузить из Shell, но не каждая платформа позволит после этого удобно войти в Setup.


8. Сборка модуля отдельно

Если у вас уже настроен EDK2 и OVMF, можно собрать только модуль:

build -p OvmfPkg/OvmfPkgX64.dsc -a X64 -t GCC5 -m ScreenshotPkg/ScreenshotDxe/ScreenshotDxe.inf

Результат обычно будет здесь:

Build/OvmfPkgX64/DEBUG_GCC5/X64/ScreenshotDxe.efi

Этот .efi можно копировать на FAT-флешку и загрузить из UEFI Shell:

fs0:
load ScreenshotDxe.efi

9. Встраивание в OVMF для удобной проверки

Чтобы драйвер был внутри OVMF и работал сразу при входе в Setup, добавьте его в платформу.

9.1. Добавьте в DSC

Откройте, например:

OvmfPkg/OvmfPkgX64.dsc

В секцию компонентов для X64, обычно:

[Components.X64]

добавьте:

ScreenshotPkg/ScreenshotDxe/ScreenshotDxe.inf

9.2. Добавьте в FDF

Откройте:

OvmfPkg/OvmfPkgX64.fdf

Найдите секцию DXE firmware volume.

Обычно это что-то вроде:

[FV.DXEFV]

или другая DXE-секция вашей платформы.

Добавьте:

INF ScreenshotPkg/ScreenshotDxe/ScreenshotDxe.inf

Если не уверены, ищите место, где уже находятся DXE-драйверы, например GraphicsConsoleDxe, ConSplitterDxe, FatPkg и т.п.


10. Сборка OVMF

Для Linux/GCC5:

. ./edksetup.sh
make -C BaseTools
build -p OvmfPkg/OvmfPkgX64.dsc -a X64 -t GCC5

Результат:

Build/OvmfPkgX64/DEBUG_GCC5/FV/OVMF_CODE.fd
Build/OvmfPkgX64/DEBUG_GCC5/FV/OVMF_VARS.fd

или единый файл:

Build/OvmfPkgX64/DEBUG_GCC5/FV/OVMF.fd

11. Проверка в QEMU

11.1. Создайте общую FAT-папку

На хосте:

mkdir -p ./uefi_share/Screenshots

QEMU умеет отдавать ordinary directory как FAT через vvfat:

-drive file=fat:rw:./uefi_share,format=raw

11.2. Запустите QEMU с OVMF

Пример для Linux:

qemu-system-x86_64 \
  -enable-kvm \
  -m 2048 \
  -drive if=pflash,format=raw,readonly=on,file=Build/OvmfPkgX64/DEBUG_GCC5/FV/OVMF_CODE.fd \
  -drive if=pflash,format=raw,file=Build/OvmfPkgX64/DEBUG_GCC5/FV/OVMF_VARS.fd \
  -drive file=fat:rw:./uefi_share,format=raw \
  -vga std \
  -serial stdio

Если используете один файл OVMF.fd, можно проще:

qemu-system-x86_64 \
  -enable-kvm \
  -m 2048 \
  -bios Build/OvmfPkgX64/DEBUG_GCC5/FV/OVMF.fd \
  -drive file=fat:rw:./uefi_share,format=raw \
  -vga std \
  -serial stdio

11.3. Войдите в Setup

В OVMF обычно нужно нажать:

  • F2, или
  • Esc, или
  • выбрать пункт Setup/Boot Manager.

Дальше нажмите клавишу, которую вы выбрали в драйвере.

По умолчанию:

F8

Скриншот должен появиться на хосте:

./uefi_share/Screenshots/shot_00000000.bmp

Если клавиша F8 перехватывается хостом или VNC-клиентом, можно послать ее через QEMU monitor:

sendkey f8

12. Проверка на реальном железе

12.1. Подготовьте USB-флешку

Формат:

FAT32

Каталог можно не создавать заранее — драйвер сам создаст:

\Screenshots

Но можно создать вручную:

Screenshots

12.2. Загрузите драйвер

Если платформа поддерживает UEFI Shell:

  1. Загрузитесь в Shell.

2. Выполните:

map -r
fs0:
load ScreenshotDxe.efi
  • Если после этого можно войти в Setup — входите и жмите F8.
  • Если войти в Setup после загрузки драйвера нельзя, драйвер нужно встраивать в firmware volume.

12.3. Secure Boot

Если включен Secure Boot, неподписанный драйвер может не загрузиться.

Варианты для лаборатории:

  • отключить Secure Boot;
  • подписать драйвер собственным ключом и добавить сертификат в db;
  • enroll-нуть хеш драйвера, если платформа это поддерживает.

Обходить Secure Boot на чужой машине не следует.


13. Что делать, если нужно сохранять на конкретный диск

Текущий код использует FindWritableRoot() и берет первый доступный для записи том. Это удобно для теста, но может выбрать не ту файловую систему, например ESP Windows.

Чтобы задать конкретный носитель, измените FindWritableRoot():

  • ищите нужный EFI_DEVICE_PATH_PROTOCOL;
  • сравнивайте USB-устройства по MSG_USB_DP;
  • или сохраняйте только на том, у которого метка/путь соответствует заданному.

Самый простой лабораторный вариант — иметь только одну подключенную FAT-флешку.


14. Если нужно снимать только активный экран

Текущий код находит все найденные GOP-протоколы. Если экранов несколько, могут сохраняться черные или неиспользуемые дисплеи.

Можно попробовать сначала захватывать GOP с ConsoleOutHandle:

Status = gBS->HandleProtocol (
                gST->ConsoleOutHandle,
                &gEfiGraphicsOutputProtocolGuid,
                (VOID **)&Gop
                );
if (!EFI_ERROR (Status)) {
  SaveGopScreen (Gop);
  return;
}

Но на некоторых платформах ConsoleOutHandle может не иметь GOP напрямую из-за console splitter. Поэтому текущий вариант с перебором всех GOP более универсален.


15. Если нужен PNG, а не BMP

BMP выбран потому, что его легко сохранить без внешних библиотек.

Если нужен PNG:

  1. Добавьте PNG-энкодер.
  2. Либо используйте стороннюю библиотеку, совместимую с EDK2.
  3. Либо реализуйте минимальный PNG writer.

Для отладки BMP обычно предпочтительнее: проще, меньше кода, меньше рисков ошибок.


16. Типичные проблемы

16.1. Драйвер не видит клавиатуру

Проверьте:

  • есть ли EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL;
  • используется ли USB/PS2 клавиатура;
  • включен ли USB legacy/USB keyboard support;
  • не загружаетесь ли вы только через serial console.

Для serial console GOP может отсутствовать, тогда скриншот невозможен.


16.2. Нет GOP

Скриншот через этот метод требует графического вывода.

Проверьте:

  • включен ли UEFI mode;
  • есть ли GOP на видеокарте;
  • не используется ли legacy VGA/CSM без GOP;
  • есть ли графический Setup.

Если Setup текстовый и работает через serial, framebuffer GOP недоступен.


16.3. Файл не создается

Причины:

  • нет FAT-файловой системы;
  • носитель только для чтения;
  • файловая система еще не подключена;
  • нет места;
  • используется NTFS/exFAT без поддержки в firmware.

Решение:

  • FAT32;
  • another USB stick;
  • выполнить map -r в Shell;
  • оставить ConnectAllControllers();
  • проверить, что том виден как fs0, fs1 и т.п.

16.4. Горячая клавиша не срабатывает

Возможно:

  • клавиша уже занята BIOS Setup;
  • клавиша обрабатывается платформой как hotkey до передачи в Setup;
  • используется другой клавиатурный стек;
  • клавиатурный протокол без SIMPLE_TEXT_INPUT_EX.

Попробуйте:

SCAN_F9
SCAN_F10
SCAN_F11
SCAN_F12
SCAN_PAUSE

Или измените код на комбинацию клавиш через KeyState.KeyShiftState.


16.5. Скриншот получается черным

Возможно:

  • выбран неактивный GOP;
  • экран погашен;
  • GOP есть, но Setup рисуется на другом дисплее;
  • захват произошел до установки видеорежима.

Решение: - нажимайте клавишу уже после отображения Setup; - снимайте только активный GOP; - уберите лишние GOP-устройства; - проверьте, что используется графический режим.


16.6. BMP открывается с неправильными цветами

Код выше сохраняет BGRX/BMP 32-bit правильно для большинства viewers.

Если цвета странные:

  • проверьте, что BMP открывается нормальным viewer;
  • попробуйте конвертировать в PNG;
  • убедитесь, что не перепутан порядок строк;
  • убедитесь, что используется 32-bit BI_RGB, а не RGB24 с padding.

16.7. Setup подвисает при сохранении

Запись большого BMP на медленную флешку может занимать время.

Особенно при:

  • 4K разрешении;
  • USB 2.0;
  • слабом firmware FAT driver.

Можно:

  • уменьшить частоту захвата;
  • использовать быстрый носитель;
  • писать raw-файл, а конвертировать позже;
  • сохранять только один активный экран.

17. Безопасность и этика

Такой драйвер может сохранять содержимое экрана до загрузки ОС. Это мощный отладочный инструмент, но использовать его нужно ответственно.

Рекомендации:

  1. Используйте только на своих устройствах или с разрешением.
  2. Не делайте захват скрытно.
  3. Не сохраняйте пароли, ключи, серийные номера и другую чувствительную информацию.
  4. Если храните скриншоты на FAT-флешке, учитывайте, что данные не шифруются.
  5. На production-машинах изменение firmware может нарушить Secure Boot, measured boot, гарантию и политики безопасности.
  6. Для коммерческих BIOS обычно требуется подпись вендора или корпоративная процедура обновления.

18. Возможные доработки

18.1. Timestamp в имени файла

Сейчас имя файла использует счетчик:

shot_00000000.bmp

Можно добавить время через:

gRT->GetTime()

Для этого подключите:

#include <Library/UefiRuntimeServicesTableLib.h>

и добавьте библиотеку в INF:

UefiRuntimeServicesTableLib

Пример имени:

shot_20260820_153012.bmp

18.2. Захват по таймеру

Если горячая клавиша не работает, можно делать автоматический скриншот каждые N секунд.

Например, в TimerNotify():

CaptureAllScreens ();

Но это быстро заполнит носитель.


18.3. Захват только одной кнопки без таймера

Если нужна минимальная задержка, можно вызывать CaptureAllScreens() прямо из ScreenshotKeyNotify(). Но это рискованно: файловый ввод-вывод из key notify может быть нестабильным на некоторых платформах.


18.4. Выбор конкретного USB-носителя

Можно анализировать device path и искать MSG_USB_DP.

Идея:

DevicePathType(Node) == MESSAGING_DEVICE_PATH
DevicePathSubType(Node) == MSG_USB_DP

Так можно писать только на USB-флешку, а не на ESP.


18.5. Автоматизация обхода Setup

Если нужно снять все страницы Setup:

  1. Используйте QEMU monitor sendkey.
  2. Или добавьте тестовый harness, который эмулирует навигацию.
  3. Или подключите внешний USB HID-эмулятор.

Автоматический проход по Setup — уже отдельная задача UI-автоматизации.


19. Краткий чек-лист

Перед проверкой убедитесь:

  • [ ] EDK2 собирается без ошибок.
  • [ ] FILE_GUID уникален.
  • [ ] Драйвер добавлен в DSC/FDF, если нужен встроенный вариант.
  • [ ] Используется FAT32.
  • [ ] Носитель доступен для записи.
  • [ ] Горячая клавиша не конфликтует с Setup.
  • [ ] Setup работает в графическом режиме.
  • [ ] Secure Boot отключен или драйвер подписан.
  • [ ] Есть место для BMP.
  • [ ] Вы тестируете на своей системе или имеете разрешение.

20. Итог

Вы получаете DXE-драйвер, который:

  • стартует рано, еще до входа в Setup;
  • регистрирует горячую клавишу;
  • по нажатию снимает framebuffer через GOP;
  • сохраняет скриншот в BMP на FAT-носитель;
  • подходит для OVMF/QEMU и может быть перенесен на реальную UEFI-платформу.

Это удобный способ для отладки BIOS Setup, автоматизации тестирования, проверки локализации и диагностики графического вывода.


Информация предоставлена в ознакомительных целях. Применение описанных настроек в системах, разработка кода должно осуществляться только после согласования с ответственными за информационную безопасность и в соответствии с требованиями ФСТЭК, ФСБ и иных уполномоченных органов.


Статью подготовил: Аверко Денис Сергеевич @Nymexis г. Омск (специалист по ЗИ)

Комментарии

Загрузка...
Если комментарии не загружаются, можете попробовать отключить блокировщик рекламы для этого сайта