Подробный гайд как написать DXE-драйвер для UEFI, который по горячей клавише снимает framebuffer
Гайд, как написать DXE-драйвер для UEFI, который по горячей клавише снимает framebuffer и сохраняет скриншот BIOS/UEFI Setup в BMP-файл на FAT-носитель.
Важно:
Делайте это только на своей системе или с письменного разрешения владельца. Драйвер работает до ExitBootServices и предназначен для отладки, тестирования Setup, локализации, автоматизации QA и т.п.
1. Общая идея
В UEFI Setup обычно рисует интерфейс через GOP — EFI_GRAPHICS_OUTPUT_PROTOCOL.
Поэтому самый переносимый способ сделать скриншот:
- Дождаться появления клавиатурного протокола
EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL. - Зарегистрировать горячую клавишу, например
F8. - По нажатию клавиши установить флаг.
- По таймеру проверить флаг и выполнить захват экрана.
- Через
GOP->Blt()скопировать framebuffer вEFI_GRAPHICS_OUTPUT_BLT_PIXEL. - Сохранить буфер как 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. Подключение драйвера к платформе
Есть два варианта:
- Встроить драйвер в прошивку.
- Загрузить драйвер вручную из 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:
- Загрузитесь в 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:
- Добавьте PNG-энкодер.
- Либо используйте стороннюю библиотеку, совместимую с EDK2.
- Либо реализуйте минимальный 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. Безопасность и этика
Такой драйвер может сохранять содержимое экрана до загрузки ОС. Это мощный отладочный инструмент, но использовать его нужно ответственно.
Рекомендации:
- Используйте только на своих устройствах или с разрешением.
- Не делайте захват скрытно.
- Не сохраняйте пароли, ключи, серийные номера и другую чувствительную информацию.
- Если храните скриншоты на FAT-флешке, учитывайте, что данные не шифруются.
- На production-машинах изменение firmware может нарушить Secure Boot, measured boot, гарантию и политики безопасности.
- Для коммерческих 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:
- Используйте QEMU monitor
sendkey. - Или добавьте тестовый harness, который эмулирует навигацию.
- Или подключите внешний 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, автоматизации тестирования, проверки локализации и диагностики графического вывода.
Информация предоставлена в ознакомительных целях. Применение описанных настроек в системах, разработка кода должно осуществляться только после согласования с ответственными за информационную безопасность и в соответствии с требованиями ФСТЭК, ФСБ и иных уполномоченных органов.