Создать собственный UEFI-загрузчик для серверной платформы на Intel Xeon IceLake без исходников, полной документации и официальной поддержки ― звучит как приключение. Мы в OpenYard решились на этот шаг, чтобы получить полный контроль над прошивкой, безопасность на уровне железа и независимость от вендоров. В статье наш путь: от первых проб с edk2 и FSP до релиза OYBoot, с реверсом драйверов, интеграцией BMC и борьбой за стабильный старт платформы.

Идея о разработке своего UEFI-загрузчика прорабатывалась достаточно стремительно. Во время обсуждений рассматривались все преимущества, недостатки, вопросы и потенциальные сложности этого мероприятия. В какой-то момент времени количество недостатков и вопросов примерно уравновесили преимущества.
Действительно, на разработку жизнеспособного загрузчика необходимо немало времени (ибо платформа Intel Xeon сложна), достаточно приличный штат разработчиков и доступ к необходимому набору документации от Intel (гайды, мануалы, описания регистров и т.д). Разумеется, ничего из перечисленного под рукой не оказалось, но был некоторый опыт в bring-up новых материнских плат, разработке UEFI-модулей и драйверов. А вот чего мы ждали: возможность контролировать функциональный состав прошивок, обеспечивать непрерывную и стабильную поддержку пользователей, гарантировать в полном объеме информационную безопасность наших аппаратных платформ. Ну и, конечно же, нельзя не упомянуть о престиже OpenYard, как разработчика и производителя серверной электроники.
Разумеется, в силу отсутствия проприетарных исходных кодов на актуальное на тот момент поколение Xeon Scalable (IceLake) единственным нашим инструментом стал edk2-framework. В качестве шаблона для проекта мы использовали также доступные opensource репозитории edk2-platforms и самый важный Intel-FSP, который предоставляет разработчикам бинарные образы FSP (Intel Firmware Support Package), являющиеся неотъемлемой частью Intel-based платформ (серверных, пользовательских, мобильных). И вот на этом этапе возник первый серьезный вопрос: как разобраться с настройками этого бинарного модуля, не имея в доступе полного набора документации от Intel, а тем более какой-либо технической поддержки. В распоряжении команды был только FSP2.0 Integration Guide и набор хедеров, приложенных в репозитории Intel-FSP для целевого процессора. Стоит отметить, что код хедера FSP сделан максимально читабельным и с неплохим набором пояснений и комментариев, что несколько компенсирует сложности в разработке без доступа к документации.
/** FSP-M Configuration
**/
typedef struct {
/** Offset 0x0040 - Customer Revision
The Customer can set this revision string for their own purpose.
**/
UINT8 CustomerRevision[32];
/** Offset 0x0060 - Bus Ratio
Indicates the ratio of Bus/MMIOL/IO resource to be allocated for each CPU's IIO.
Default 0x1
**/
UINT8 BusRatio[8];
/** Offset 0x0068 - D2K Credit Config
Set the D2K Credit Config. 1: Min,<b>2: Med (Default), 3: Max.
1:Min, 2:Med, 3:Max
**/
UINT8 D2KCreditConfig;
/** Offset 0x0069 - Snoop Throttle Config
Set the Snoop Throttle Config. <b>0: Disable(Default)</b>, 1: Min, 2: Med, 3: Max
0:Disable, 1:Min, 2:Med, 3:Max
**/
UINT8 SnoopThrottleConfig;
/** Offset 0x006A - Snoop Throttle Config
Set the Snoop All Core Config. <b>0: Disable(Default)</b>, 1: Enable, 2: Auto
0:Disable, 1:Enable, 2:Auto
**/
UINT8 SnoopAllCores;
/** Offset 0x006B - Legacy VGA Socket
Socket that claims the legacy VGA range. Default: Socket 0
**/
UINT8 LegacyVgaSoc;
/** Offset 0x006C - Legacy VGA Stack
Stack that claims the legacy VGA range. Default: Stack 0
Первой важной задачей во всей этой истории была корректная и достаточная конфигурация и интеграция FSP в наш проект. Здесь стоит сказать, что мы пошли по самому простому алгоритму: внесение изменений в программный код конфигурации FSP, сборка нашего бинарного образа UEFI, запуск на целевой платформе и контроль состояния загрузки в отладочной консоли. По большому счету такой алгоритм называется «метод проб и ошибок». Количество итераций, которые мы провели для успешного перехода хотя бы в UEFI-payload назвать страшно. Наибольшее время мы затратили на поиск оптимальных настроек FSP для корректного исполнения MRC (Memory Reference Code). Результатом этого этапа стала загрузка нашей системы в оболочку Efi (Efi shell)… Но только в отладочной консоли. Как позже выяснилось, у нас не работала видеоконсоль в силу отсутствия в нашей сборке корректного Efi-драйвера.
С этого момента история приобрела иные краски. Закончился этап игры «в угадайку» и мы приступили к превращению полуфабриката в конечный продукт. К этому моменту у команды появилось больше понимания об особенностях целевой платформы, о поведении FSP, а также появилось больше уверенности в своих силах. Это позволило нам несколько распараллелить разработку.
В части доработки необходимых драйверов команде пришлось осваивать технологии реверс-инжиниринга. Далеко не для всех драйверов было желание (да и возможность разрабатывать код самим). Как пример UEFI GOP ― драйвер: команда вытащила этот драйвер из уже имеющейся прошивки от нашего ODM партнера. А для правильной интеграции полученного бинарника в нашу сборку мы разработали код по примеру, взятому из одного из старых проприетарных проектов.
Пока одна часть команды боролась с созданием минимального работоспособного загрузчика, вторая часть разрабатывала функциональные модули, такие как: функции управления портами USB, PCIe и SATA платформы, устройствами шифрования TPM2.0, модуль настройки порта менеджмента (BMC).
Все это необходимо было завернуть в привычный и user-friendly интерфейс, который пользователям более знаком, как Setup Utility. Получилось так:



Отдельно отмечу задачи, связанные с обязательным функционалом взаимодействия нашего OYBoot с BMC (Baseboard Management Controller) по интерфейсу IPMI, а также трансляции полной SMBIOS-таблицы с целью обеспечения инвенторики оборудования. Тут сложность оказалась в том, что SMBIOS опять же надо было передавать в «черный ящик» под названием AMI MegaRAC. На помощь нам пришел уже повышенный опыт реверс-инжиниринга. После непродолжительного исследования команда точно знала все о механизме транспортировки SMBIOS-таблицы в пространство памяти BMC, а также почти все о структуре разделов этой таблицы. Разумеется, помимо стандартных типов (информация о системе, процессорах и памяти) нам пришлось создавать OEM-разделы, со структурами, понятными MegaRAC.
EFI_STATUS
EFIAPI
GetSmbiosTable(VOID** SmbiosTableAddress, UINT32* SmbiosTableSize)
{
EFI_STATUS Status = EFI_NOT_FOUND;
//
// Get SMBIOS table from System Configure table
//
Status = EfiGetSystemConfigurationTable(&gEfiSmbiosTableGuid, (VOID**)&mSmbiosTable);
if ((mSmbiosTable != NULL) && (!EFI_ERROR(Status))) {
SmbiosTableAddress = (UINT8)(UINTN)(mSmbiosTable->TableAddress);
*SmbiosTableSize = (UINT32)mSmbiosTable->TableLength;
return EFI_SUCCESS;
}
//
// Get SMBIOS table 3 from System Configure table
//
Status = EfiGetSystemConfigurationTable(&gEfiSmbios3TableGuid, (VOID**)&mSmbios64BitTable);
if ((mSmbios64BitTable != NULL) && (!EFI_ERROR(Status))) {
SmbiosTableAddress = (UINT8)(UINTN)(mSmbios64BitTable->TableAddress);
*SmbiosTableSize = (UINT32)mSmbios64BitTable->TableMaximumSize;
return EFI_SUCCESS;
}
return Status;
}
VOID
EFIAPI
ReadyToBootNotify(
EFI_EVENT Event,
VOID* Context)
{
EFI_STATUS Status;
EFI_GRAPHICS_OUTPUT_PROTOCOL *Gop;
VOID* SmbiosTableAddress = NULL;
UINT32 SmbiosTableSize = 0x00;
UINT8 Response[0x10];
UINT8 ResponseSize = 0x10;
Status = gBS->LocateProtocol(
&gEfiGraphicsOutputProtocolGuid,
NULL,
(VOID**)&Gop
);
if (EFI_ERROR(Status)) {
return;
}
VOID* Destination = (VOID*)(UINTN)Gop->Mode->FrameBufferBase;
Status = GetSmbiosTable(&SmbiosTableAddress, &SmbiosTableSize);
DEBUG((DEBUG_INFO, "GetSmbiosTable, Status = %r\n", Status));
if (!EFI_ERROR(Status))
{
DEBUG((DEBUG_INFO, "Address = 0x%p, Size = 0x%x bytes\n", SmbiosTableAddress, SmbiosTableSize));
CopyMem(Destination, SmbiosTableAddress, SmbiosTableSize);
}
else
{
return;
}
ZeroMem(Response, 0x10);
UINT8 IpmiCommandData1[] = IPMI_COMMAND_DATA_GET_SHARED_MEMORY_ADDRESS;
Status = IpmiSendCommand(
IPMI_COMMAND_GET_SHARED_MEMORY_ADDRESS,
IpmiCommandData1,
sizeof(IpmiCommandData1),
Response,
&ResponseSize
);
DEBUG((DEBUG_INFO, "IpmiSendCommand: Get shared memory address, Status = %r\n", Status));
if (!EFI_ERROR(Status))
{
ZeroMem(Response, 0x10);
ResponseSize = 0x0A;
UINT8 IpmiCommandData2[] = IPMI_COMMAND_DATA_DATA_READY;
Status = IpmiSendCommand(
IPMI_COMMAND_DATA_READY,
IpmiCommandData2,
sizeof(IpmiCommandData2),
Response,
&ResponseSize
);
DEBUG((DEBUG_INFO, "IpmiSendCommand: Smbios data ready, Status = %r\n", Status));
}
gBS->Stall(1 1000 1000);
}
Исходный код выше реализует разработанную нами процедуру автоматического определения адреса в пространстве памяти BMC для последующей передачи SMBIOS.
Полгода назад у нас не было ничего, кроме идеи, опыта bring-up и пары утилит с открытым кодом. Сегодня у нас есть OYBoot ― собственный UEFI-загрузчик, полностью адаптированный под наши серверы на Xeon IceLake, с поддержкой всех необходимых функций и глубокой интеграцией в инфраструктуру. Но ещё важнее ― мы получили бесценный опыт работы с FSP, освоили тонкости edk2, научились реверсить драйверы и наладили командную работу в условиях ограниченных ресурсов. Для нас это доказательство того, что даже в мире закрытых платформ можно добиться полного контроля над железом ― если есть упорство, любопытство и готовность к экспериментам.
CodeRush
Непонятно, почему вы прошивку полноценную называете "загрузчиком".
У вас здесь прошивка, собранная из FSP и EDK2 на минималках, достаточная, чтобы с одной стороны загружать вашу целевую ОС, а с другой - общаться с IPMI от AMI так, чтобы он не слишком сильно матерился. "Загрузчиком" у вас является следующая стадия, т.е. то, на что ваша прошивка передает управление (bootmgfw.efi, shell.efi, grub.efi, ядро Linux с EFI_STUB, etc.)
Лучше убрать отсюда слово "полного", потому что в таком виде это смешно. У вас весь код фазы PEI и часть кода DXE (компоненты FSP) - закрытые и под закрытыми лицензиями, а дали их вам на условиях AS IS фактически "погонять-покрутить". Не говоря уже про компоненты Intel ME, без которых ваша платформа не загрузится, и о которых вы даже не упоминаете, тоже выданные вам в виде BLOBа на тех же условиях.
Никто не спорит с тем, что на современных платформах Intel, для которых их милостью доступен FSP, можно написать свою относительно работающую прошивку, но при этом до "полного контроля на железом" на них все еще как раком до Китая.
Опять же, я очень рад, что российские компании наконец-то начали делать что-то свое (пусть и на основе FSP и EDK2), а не говнокод IBV покупать, и по существу претензий у меня нет - так держать, молодцы, ребята!