Создать собственный 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, научились реверсить драйверы и наладили командную работу в условиях ограниченных ресурсов. Для нас это доказательство того, что даже в мире закрытых платформ можно добиться полного контроля над железом ― если есть упорство, любопытство и готовность к экспериментам.