Подробный гайд: EDK II изнутри: написание DXE-драйвера, отладка в QEMU и fallback-сценарии UEFI

Углублённый гайд по EDK II: написание DXE-драйвера, отладка кода в QEMU и OVMF, разбор архитектуры и типовых fallback-сценариев загрузки UEFI.

2026.08.22                  


Подробный гайд: EDK II изнутри: написание DXE-драйвера, отладка в QEMU и fallback-сценарии UEFIПодробный гайд: EDK II изнутри: написание DXE-драйвера, отладка в QEMU и fallback-сценарии UEFI Разработка под UEFI — это одна из самых сложных и интересных областей системного программирования. EDK II (EFI Development Kit II) является индустриальным стандартом для создания прошивок.


Часть 1. Углублённый разбор фазы DXE (Driver Execution Environment)

Процесс загрузки UEFI делится на несколько фаз: SEC (Security) -> PEI (Pre-EFI Initialization) -> DXE (Driver Execution Environment) -> BDS (Boot Device Selection) -> TSL (Transient System Load) -> RT (Runtime).



Фаза DXE — это «сердце» прошивки. Именно здесь инициализируется большинство устройств, выстраивается абстракция оборудования и подготавливается среда для загрузчика ОС.


Ключевые концепции DXE:

1. DXE Core (DxeCore):

Это не драйвер, а ядро фазы. Оно выделяет память, инициализирует базовые сервисы (Boot Services, Runtime Services) и запускает DXE Dispatcher (Диспетчер).

2. DXE Dispatcher:

Загружает DXE-драйверы из Firmware Volumes (FV). Порядок загрузки жестко не задан, а определяется DEPEX (Dependency Expressions) — выражениями зависимостей в .inf файлах. Драйвер загрузится только если все протоколы, указанные в его DEPEX, уже установлены в системе.

3. Handle Database (База дескрипторов):

Центральная шина данных DXE. Драйверы не вызывают друг друга напрямую. Они «устанавливают» (Install) Протоколы (наборы функций и данных) на Дескрипторы (Handle). Другие драйверы «открывают» (Open) эти протоколы по GUID.

4. События и уведомления (Events & Notifications):

Механизм асинхронного взаимодействия. Драйвер может создать событие и повесить на него нотификатор, который сработает, например, при появлении определенного протокола в базе (Event Notification on Protocol Installation).


Часть 2. Практика: Пишем собственный DXE-драйвер

Напишем простой DXE-драйвер, который выводит сообщение в консоль и устанавливает фиктивный пользовательский протокол, чтобы продемонстрировать работу с базой дескрипторов.


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

Создадим директорию MyCustomDxePkg/MyCustomDxe/.


2. Файл описания модуля (MyCustomDxe.inf)

[Defines]
  INF_VERSION    = 0x00010005
  BASE_NAME      = MyCustomDxe
  FILE_GUID      = 12345678-9ABC-DEF0-1234-56789ABCDEF0
  MODULE_TYPE    = DXE_DRIVER
  VERSION_STRING = 1.0
  ENTRY_POINT    = MyCustomDxeEntry

[Sources]
  MyCustomDxe.c

[Packages]
  MdePkg/MdePkg.dec
  MdeModulePkg/MdeModulePkg.dec

[LibraryClasses]
  UefiDriverEntryPoint
  UefiBootServicesTableLib
  UefiLib
  DebugLib
  MemoryAllocationLib
  BaseMemoryLib

[Protocols]
  gEfiCustomDummyProtocolGuid  ## PRODUCES

[Depex]
  gEfiBootServicesTableGuid    # Ждем инициализации Boot Services

3. Исходный код (MyCustomDxe.c)

#include <Uefi.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiLib.h>
#include <Library/DebugLib.h>
#include <Library/MemoryAllocationLib.h>

// Определяем GUID нашего протокола
#define EFI_CUSTOM_DUMMY_PROTOCOL_GUID \
  { 0xA1B2C3D4, 0xE5F6, 0x7890, { 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0 } }

EFI_GUID gEfiCustomDummyProtocolGuid = EFI_CUSTOM_DUMMY_PROTOCOL_GUID;

// Структура нашего протокола
typedef struct _MY_CUSTOM_PROTOCOL {
    UINT32 Version;
    EFI_STATUS (EFIAPI *DoSomething)(IN CHAR16 *Message);
} MY_CUSTOM_PROTOCOL;

// Реализация функции протокола
EFI_STATUS EFIAPI MyDoSomething(IN CHAR16 *Message) {
    Print(L"[MyCustomProtocol] %s\n", Message);
    return EFI_SUCCESS;
}

// Глобальный экземпляр протокола
MY_CUSTOM_PROTOCOL mMyCustomProtocol = {
    0x00010000,
    MyDoSomething
};

// Точка входа DXE-драйвера
EFI_STATUS EFIAPI MyCustomDxeEntry(
    IN EFI_HANDLE ImageHandle,
    IN EFI_SYSTEM_TABLE *SystemTable
) {
    EFI_STATUS Status;
    EFI_HANDLE Handle = NULL;

    DEBUG((DEBUG_INFO, "MyCustomDxe: Driver Entry Point reached!\n"));
    Print(L"Hello DXE World from MyCustomDxe!\n");

    // Устанавливаем протокол в базу дескрипторов
    Status = gBS->InstallMultipleProtocolInterfaces(
        &Handle,
        &gEfiCustomDummyProtocolGuid, &mMyCustomProtocol,
        NULL
    );

    if (EFI_ERROR(Status)) {
        DEBUG((DEBUG_ERROR, "MyCustomDxe: Failed to install protocol. Status: %r\n", Status));
        return Status;
    }

    DEBUG((DEBUG_INFO, "MyCustomDxe: Protocol installed successfully.\n"));
    return EFI_SUCCESS;
}



4. Интеграция в OVMF

Чтобы драйвер попал в прошивку, нужно добавить его в файлы платформы OVMF.

1. В OvmfPkg/OvmfPkgX64.dsc в секцию [Components] добавляем:

   MyCustomDxePkg/MyCustomDxe/MyCustomDxe.inf

2. В OvmfPkg/OvmfPkgX64.fdf в секцию [FV.DXEFV] (или FV.DXEFSM) добавляем:

   INF MyCustomDxePkg/MyCustomDxe/MyCustomDxe.inf

3. Не забываем добавить MyCustomDxePkg/MyCustomDxePkg.dec в корневой .dsc файл, если вы создаете отдельный пакет, либо просто положите .inf в существующий пакет, например, OvmfPkg.


Часть 3. Отладка в OVMF/QEMU

Отладка UEFI отличается от отладки приложений в ОС. Здесь нет стандартного printf в ОС, а память может быть переотображена.

Метод 1: Serial Console (Основной и самый надежный)

В EDK II используется макрос DEBUG(). В OVMF его можно перенаправить на последовательный порт (COM1).

1. Сборка OVMF с отладкой:

   build -p OvmfPkg/OvmfPkgX64.dsc -a X64 -t GCC5 -D DEBUG_ON_SERIAL_PORT

2. Запуск QEMU:

   qemu-system-x86_64 -bios Build/OvmfX64/DEBUG_GCC5/FV/OVMF.fd -serial stdio -nographic -m 2G

Результат:

Вы увидите весь лог загрузки, включая сообщения из DEBUG((DEBUG_INFO, ...)) вашего драйвера, прямо в терминале.


Метод 2: GDB (Для пошаговой отладки)

Отладка DXE через GDB сложна из-за того, что DXE-драйверы загружаются в память по динамическим адресам (PE/COFF relocation).

1. Сборка с отладочными символами:

Убедитесь, что в target.txt или в команде сборки указан TARGET=DEBUG.

2. Запуск QEMU с GDB-сервером:

   qemu-system-x86_64 -bios Build/OvmfX64/DEBUG_GCC5/FV/OVMF.fd -s -S -serial stdio -nographic

(Флаг -S замораживает CPU на старте, -s открывает порт 1234 для GDB).

3. Подключение GDB:

   gdb
   (gdb) target remote :1234

4. Загрузка символов (Самое сложное):

Так как DXE Core релоцирует драйверы, вам нужно узнать базовый адрес загрузки вашего модуля.

В OVMF при загрузке драйвера в serial-логе есть строка:

Loading driver at 0x0007F000000 EntryPoint=0x0007F001234 MyCustomDxe.efi

Используем этот адрес (в примере 0x7F000000) для загрузки символов:

   (gdb) add-symbol-file Build/OvmfX64/DEBUG_GCC5/X64/MyCustomDxe.debug 0x7F000000
   (gdb) break MyCustomDxeEntry
   (gdb) continue

Совет:

Для автоматизации в репозитории EDK II есть скрипты (например, uefi-gdb.py), которые помогают парсить логи и подгружать символы, но ручной метод через add-symbol-file работает безотказно.


Часть 4. Разбор типовых Fallback-сценариев загрузки

Что делает прошивка, если что-то пошло не так? UEFI Specification и архитектура EDK II предусматривают несколько уровней отказа.


Сценарий 1: Отсутствие или поломка загрузчика ОС (BDS Fallback)

Ситуация:

Переменная BootOrder пуста, или указанные в BootXXXX пути к EFI-файлам не существуют (например, удалили grubx64.efi).

Действия BDS (Boot Device Selection):

  1. BDS перебирает BootOrder. Если ни один путь не сработал, срабатывает Removable Media Fallback.
  2. Прошивка сканирует все подключенные блочные устройства (USB, CD, HDD) на наличие файловой системы EFI (FAT32).
  3. На каждом устройстве ищется жестко заданный путь: \EFI\BOOT\BOOTX64.EFI (для x86_64), BOOTAA64.EFI (для ARM64) и т.д.
  4. Если файл найден, он запускается. Именно поэтому загрузочные флешки Windows/Linux всегда кладут свой загрузчик в \EFI\BOOT\.



Сценарий 2: Повреждение NVRAM (Clear CMOS / Battery Dead)

Ситуация:

Переменные UEFI (включая BootOrder) повреждены или сброшены.

Действия PEI/DXE:

  1. Драйвер VariableDxe при инициализации проверяет CRC заголовка NVRAM.
  2. Если CRC не сходится или хранилище пусто, срабатывает NVRAM Recovery.
  3. Прошивка ищет в Firmware Volume (FV) секцию NV_DEFAULT_STORE. Это слепок заводских настроек.
  4. Переменные восстанавливаются из дефолтного слепка. BootOrder сбрасывается в заводское состояние (обычно: сначала сеть/PXE, потом CD, потом жесткий диск).
  5. Пользователь видит сообщение "NVRAM cleared" или "Press F1 to enter Setup".

Сценарий 3: Сбой при обновлении прошивки (Capsule Update Failure)

Ситуация:

Обновление BIOS (Capsule) прервалось из-за потери питания. Прошивка "окирпичена".

Действия (Зависит от вендора, но стандарт EDK II предполагает следующее):

  1. В современных системах Capsule обрабатывается не в DXE, а в PEI (модуль CapsulePei или CapsuleOnDisk), чтобы обновить микрокод и MRC (Memory Reference Code) до инициализации памяти.
  2. Если обновление прервалось, при следующем включении SEC/PEI проверяет статус Capsule.
  3. Аппаратный Fallback: На материнских платах используется Dual BIOS (две микросхемы SPI или две области в одной). Если основная область не проходит проверку CRC на этапе SEC, управление передается в Backup BIOS, который восстанавливает основную область.

Программатный Fallback (Recovery Mode):

Если аппаратного бэкапа нет, SEC ищет специальный Recovery-образ на USB (обычно с именем типа BIOS.fd или CREATIVE.EFI). Если находит — прошивает заново. Если нет — система уходит в бесконечный цикл перезагрузки (Halt).


Сценарий 4: Нарушение Secure Boot

Ситуация: Загрузчик ОС не подписан доверенным ключом, или подпись повреждена.

Действия DXE/BDS:

  1. В фазе DXE загружается SecurityPkg (модули DxeImageVerificationLib).
  2. Когда BDS пытается загрузить EFI-файл, он вызывает хук безопасности.
  3. Хэш файла сравнивается с базой db (доверенные ключи) и проверяется, нет ли его в dbx (отозванные ключи).

4. Fallback: Если проверка не пройдена, BDS не передает управление файлу.

  • Если включен UEFI Shell, прошивка может вывести предупреждение и предложить пользователю вручную добавить хэш в db (Enroll Signature).
  • Если Shell отключен, загрузка прерывается, на экран выводится красное окно "Secure Boot Violation", и система зависает или уходит в BIOS Setup.

Важно

  1. DXE — это событийно-ориентированная среда. Пишите драйверы так, чтобы они не зависели от жесткого порядка загрузки, а полагались на DEPEX и уведомления о появлении протоколов.
  2. Отладка: Всегда начинайте с DEBUG() и Serial-консоли. GDB в UEFI — это мощный, но трудоемкий инструмент из-за релокации PE-файлов.
  3. Надежность: Проектируя прошивку, всегда думайте о fallback-сценариях. Что будет, если NVRAM сотрется? Что будет, если пользователь вставит отформатированный USB? UEFI Specification дает ответы на эти вопросы, и EDK II предоставляет базовые реализации (например, BdsDxe), которые вы можете переопределить под свои нужды.

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


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

Комментарии

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