Речь пойдет об особенности PI спецификации – архитектурных протоколах, которые осуществляют портабельность DXE core и позволяют переносить всю реализацию DXE стадии на платформы эмуляции UEFI, такие как legacy BIOS (EDK/DuetPkg) и Win32 (EDK/Nt32Pkg).
Меня всегда интересовало, почему DuetPkg и Nt32Pkg, судя по их DSC файлам, по большей части состоят из бинарных модулей, ничего не знающих про эмуляцию. Наример оба эмулятора использую общую реализацию всего DXE рантайма. Как такое работает?
Согласно спецификации UEFI инициализация платформы и подготовка ее к загрузке ОС осуществляется в несколько стадий:
Весь API, который описан в UEFI спецификации становится (полностью) доступен на стадии BDS (Boot Device Selection), когда начинает работать boot manager. Реализация этого API и его инициализация происходит, в общем случае, на стадии DXE – driver execution environment. На стадии DXE работаю несколько основных компонентов, которые нас интересуют в контексте темы поста:
- DXE Core
Ядро DXE стадии, получает управление после PEI, разворачивает реализацию базы данных хендлов.
- DXE Dispatcher
Занимается загрузкой драйверов из firmware volume, который был проинициализирован на стадии PEI.
Референсную реализацию DXE можно найти в EDK/MdeModulePkg/Core/Dxe.
DXE Core реализует EFI_SYSTEM_TABLE и все сервисы из EFI_BOOT_SERVICES и EFI_RUNTIME_SERVICES за счет опоры на EFI architecture protocols:
Картинка выше перегружена деталями, но в центре находится реализация DXE, которая зависит от набора архитектурных протоколов таких как EFI_CPU_ARCH_PROTOCOL, EFI_TIMER_ARCH_PROTOCOL и так далее. Эти протоколы описаны в PI спецификации и немногим отличаются от протоколов из UEFI спецификации. Различия есть в драйверах, которые их реализуют: это обычные DXE boot service / DXE runtime service драйвера, однако т.к. опубликованные ими протоколы являются опорой для реализации основных boot и runtime сервисов, то они не могут рассчитывать на их полный набор на некоторых этапах своего выполнения.
Становится понятной роль архитектурных протоколов – они абстрагируют базовое железо конкретной архитектурной платформы и позволяют коду в DXE core опираться на эти абстракции в реализации основных сервисов, например:
- Реализация рантайм сервиса EFI_RUNTIME_SERVICES::GetTime() опирается на архитектурный протокол EFI_REAL_TIME_CLOCK_ARCH_PROTOCOL для доступа к аппаратному устройству wall-time clock
- Вся реализация ивентов в EFI_BOOT_SERVICES использует EFI_TIMER_ARCH_PROTOCOL для генерирования периодических прерываний по таймеру.
- EFI_CPU_ARCH_PROTOCOL используется для синхронизации кешей процессора и реализации сервисов управления памятью
Драйверы, реализующие эти протоколы, как правило находятся в firmware volume, который инициализируется на стадии PEI и информация о котором передается в DXE Core посредством списка Hand-off Block структур (HOB list), что показано в верхней части картинки выше. Я не буду сейчас заострять внимание на деталях HOB списка, скажу только что это каждый HOB представляет из себя блок данных и GUID, который позволяет интерпретировать эти данные различными клиентами на стадии инициализации DXE. При помощи HOB передается информация о доступной памяти, memory mapped firmware volume и т.п.
Firmware volume был упомянут уже не раз, но его определение так и не было дано до сих пор. Firmware volume (FV) это структурированная база данных исполняемых модулей DXE, т.е. драйверов и приложений. База данных FV адресует образы по GUIDу и хранит информацию о зависимостях между различными модулями, а так же т.н. a priori list – список GUIDов образов, которые нужно загрузить при инициализации DXE в строго определенной последовательности. Драйвера архитектурных протоколов как правило и находятся в a priori list. Физически на реальной системе FV находятся в ROM и доступ к нему предоставляется через замапленый диапазон адресов физический памяти. Маппинг осуществляется на стадии PEI и информация о нем передается в HOB листе.
Таким образом при старте DXE код может опираться только на рабочую физическую память, проинициализированную на стадии PEI и описанную в HOB листе. Этого достаточно для инициализации базы данных хендлов, доступа к FV, загрузке драйверов из FV посредством DXE Dispatcher и инициализацию всех UEFI сервисов.
Становится понятным список задач, которые должен выполнить эмулятор, чтобы загрузить общий код DXE:
- Реализация всех архитектурных протоколов, на который опирается DXE Core
- Реализация firmware volume, доступа к нему и загрузку бинарных образов в среду эмуляции.
- Реализация дополнительных драйверов, таких как block io, GOP, консолей и т.п.
- Формарование корректного HOB листа и передача управления в DXE.
В этом и заключается полезная нагрузка DuetPkg и Nt32Pkg. К примеру Nt32Pkg реализует архитектурные протоколы и драйвера основных UEFI протоколов через сервисы Win32 и хранит FV как папку на файловой системе предоставляя доступ через FvbServiceRuntimeDxe драйвер. Реализацию аналогичных компонентов можно найти и в DuetPkg, хотя там все сложнее из-за специфики эмулятора UEFI поверх legacy BIOS, но общая схема остается неизменной.