- PVSM.RU - https://www.pvsm.ru -
а вот статей про “Hello, World” на UEFI да с графикой действительно не хватает. Больше того — я таких вообще не припомню.» (MinimumLaw [3])
Под катом мы пошагово перепишем ту бутсекторную демку под UEFI, и она будет работать в полноцветном видеорежиме с высоким разрешением. С другой стороны, вместо 512 байт она будет занимать несколько десятков КБ.
Четыре года назад DarkTiger [4] постил туториал [5] о разработке под UEFI в Visual Studio, и даже опубликовал шаблон среды [6], позволяющий начать разработку, не ломая голову над настройками edk2: «Достаточно дать команду git clone ...
в корневом каталоге диска, и это на самом деле все, среда будет полностью установлена и готова к работе.» Шаблон этот был жёстко привязан не только к корневому каталогу, но и к Visual Studio 2015, к древней версии edk2, и к 32-битной компиляции. Чтобы работать в Visual Studio 2019, поддержка которой появилась в более новых версиях edk2, шаблон понадобится осовременить. Ещё одно изменение за эти четыре года — то, что для сборки в edk2 стал необходим Python.
Осовремененный шаблон (170 МБ трафика, 600 МБ на диске) развёртывается командой:
git clone --depth 1 --recursive --shallow-submodules https://github.com/tyomitch/uefi
PYTHON_HOME
, нужную для edk2;C:FW
в файлах vcxproj заменён на $(SolutionDir)..;
--nt32
передаются Rebuild VS2019
: первый указывает на необходимость скомпилировать edk2BaseToolsBinWin32
(несмотря на название параметра, компиляция выполняется инкрементально); второй — на используемый тулчейн.Конфигурация проекта по-прежнему называется “Win32”; на целевую платформу, фактически используемую при компиляции, это никак не влияет.
Перед сборкой нужно поменять внутри edk2BaseToolsConftarget.template
значения TARGET_ARCH
на X64
и TOOL_CHAIN_TAG
на VS2019
; после этого можно открывать VSNT32.sln
, жать F5, и всё скомпилируется и запустится. Чтобы удостовериться, что среда полноценно работает, введите в UEFI Shell команду fs0:HelloWorld.efi
:
Модуль HelloWorld мы и возьмём за основу для нашей демки.
Отрисовываемая демкой линия — это вертикально растянутая архимедова спираль [7], и для расчёта координат её точек Chris Fallin [8] использовал инструкции fcos
и fsin
. Увы, но тригонометрические функции в Си не встроены: они относятся к стандартной библиотеке, а её из edk2 исключили [9]. Нет в MSVC и инлайн-ассемблера для x64, так что функцию SinCos
придётся реализовывать в отдельном ассемблерном файле, следуя примеру Benjamin Kietzman [10]. Поместите его файл SinCos.asm рядом с исходником HelloWorld.c в каталоге edk2MdeModulePkgApplicationHelloWorld
, а в файл проекта HelloWorld.inf допишите секцию:
[Sources.X64]
SinCos.asm
.code
PUBLIC SinCos
; void SinCos(double AngleInRadians, double *pSinAns, double *pCosAns);
angle_on_stack$ = 8
SinCos PROC
movsd QWORD PTR angle_on_stack$[rsp], xmm0 ; argument angle is in xmm0, move it to the stack
fld QWORD PTR angle_on_stack$[rsp] ; push angle onto the FPU stack
fsincos
fstp QWORD PTR [r8] ; store/pop cosine output argument
fstp QWORD PTR [rdx] ; store/pop sine output argument
ret 0
SinCos ENDP
END
Попутно можете удалить из HelloWorld.inf ненужные нам секции [FeaturePcd]
, [Pcd]
, [UserExtensions.TianoCore."ExtraFiles"]
, и упоминания HelloWorldStr.uni и PcdLib. Все эти штуки, связанные с локализацией строк и настройками (PCD — это Platform Configuration Database), нам не помешают, но и не пригодятся. Поэтому же можно удалить и #include <Library/PcdLib.h>
, и определение mStringHelpTokenId
из HelloWorld.c; в начало этого файла надо добавить объявления:
#include <Library/UefiBootServicesTableLib.h>
#define M_PI 3.14159265358979323846
extern void SinCos(double AngleInRadians, double *pSinAns, double *pCosAns);
int _fltused;
Символ _fltused
должен быть объявлен в каждой MSVC-программе, использующей double
. Обычно его экспортирует стандартная библиотека, но нам его приходится объявлять вручную.
Стандартный для UEFI графический API называется GOP (Graphics Output Protocol), и он крайне прост: поддерживается ровно одна функция вывода, Blt
, копирующая прямоугольный блок пикселей между памятью и экраном. Кроме этого, может быть доступен прямой доступ к видеопамяти, и тогда отображение пикселя на экране — это просто запись UINT32
по нужному адресу. Спираль-ёлочку удобно рисовать попиксельно, напрямую в видеопамять:
EFI_STATUS
EFIAPI
UefiMain (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
EFI_STATUS efiStatus;
EFI_GUID gopGuid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID;
EFI_GRAPHICS_OUTPUT_PROTOCOL *gop;
double sin, cos;
// получим указатель на протокол
efiStatus = gBS->LocateProtocol(&gopGuid, NULL, (void**)&gop);
if (EFI_ERROR(efiStatus)) {
Print(L"Unable to locate GOPn");
return EFI_NOT_STARTED;
}
UINT32 *video = (UINT32*)(UINTN)gop->Mode->FrameBufferBase;
// отключим мерцающий курсор
gST->ConOut->EnableCursor(gST->ConOut, FALSE);
double tree_height_factor = gop->Mode->Info->VerticalResolution * .8;
double tree_width_factor = gop->Mode->Info->VerticalResolution * .5;
double tree_width_base = gop->Mode->Info->HorizontalResolution * .5;
// код отрисовки в точности соответствует ассемблерной версии от Chris Fallin
for (UINTN tick = 0; ; tick++) {
gST->ConOut->ClearScreen(gST->ConOut);
for (double t = 0; t < 1; t += .001) {
double width = t * tree_width_factor;
double w = 2 * M_PI * 5;
double p = 2 * M_PI * tick / 36; // (0.5 revs / sec)
double angle = w * t + p;
SinCos(angle, &sin, &cos);
double x = width * cos + tree_width_base;
double z = width * sin;
double y = t * tree_height_factor;
UINTN x_disp = (UINTN)(x + z * .5);
UINTN y_disp = (UINTN)(y + z * .25);
if (y_disp < gop->Mode->Info->VerticalResolution) {
UINTN coord = gop->Mode->Info->PixelsPerScanLine * y_disp + x_disp;
UINT32 pixel = ((int)(t * 1000) % 2) ? 0x00FF00 : 0xFF0000;
video[coord] = pixel;
}
}
gBS->Stall((UINTN)(65536 / (105. / 88.))); // ~55 ms, to match IBM PC timer
}
}
Увы, включённый в состав edk2 эмулятор не поддерживает прямой доступ к видеопамяти, так что в поле gop->Mode->FrameBufferBase
будет NULL
. Чтобы тестировать код, требующий прямого доступа к видеопамяти, можно использовать улучшенную версию эмулятора [11], собранную Alex Ionescu: запускается он командой:
qemu.exe -drive file=OVMF_CODE-need-smm.fd,if=pflash,format=raw,unit=0,readonly=on -drive file=OVMF_VARS-need-smm.fd,if=pflash,format=raw,unit=1 -drive file=fat:rw:…edk2BuildEmulatorX64DEBUG_VS2019X64,media=disk,if=virtio,format=raw -drive file=UefiShell.iso,format=raw -m 512 -machine q35,smm=on -nodefaults -vga std -global driver=cfi.pflash01,property=secure,value=on -global ICH9-LPC.disable_s3=1
— и каталог с результатами сборки будет подмонтирован как fs1:
Сразу заметны три проблемы, унаследованные из бутсекторной демки:
gST->ConOut->ClearScreen
, а затем на нём по одному зажигаются пиксели спирали-ёлочки. Вызываемое этим мерцание заметно даже на гифке. Чтобы от него избавиться, ёлочку надо отрисовывать в невидимый буфер, а затем вызовом Blt
отправлять на экран одним целым. Мы же объявим мерцание воображаемых гирлянд тёплой и ламповой фичей демки, и оставим его как есть. EFI_INPUT_KEY Key;
efiStatus = gST->ConIn->ReadKeyStroke(gST->ConIn, &Key);
if (!EFI_ERROR(efiStatus)) {
return efiStatus;
}
if (y_disp < gop->Mode->Info->VerticalResolution) {
UINTN coord = gop->Mode->Info->PixelsPerScanLine * y_disp + x_disp;
double color = t * 50;
color -= (INTN)color;
UINT32 pixel = (UINT32)(color * 255) << 8;
pixel |= (UINT32)((1. - color) * 255) << 16;
video[coord] = pixel;
video[coord - 1] = pixel;
video[coord + 1] = pixel;
if (y_disp > 0)
video[coord - gop->Mode->Info->PixelsPerScanLine] = pixel;
if (y_disp < gop->Mode->Info->VerticalResolution - 1)
video[coord + gop->Mode->Info->PixelsPerScanLine] = pixel;
}
Изображение, выводимое рядом со спиралью-ёлочкой в бутсекторной демке, захардкожено прямо в ассемблерном исходнике простынью из определений db
. Это неудобно: как такое изображение редактировать? Гораздо удобнее работать с BMP-файлом. Такую возможность даёт HII Database (Human Interface Infrastructure), вскользь упомянутая в туториале от DarkTiger [4] как хранилище шрифтов, доступных UEFI-приложениям. Изображения, как и шрифты, включаются в PE-образ приложения как ресурсы. Для того, чтобы добавить в ресурсы BMP-изображение, нужно несколько неочевидных шагов:
#image IMG_LOGO ruvds.bmp
— она задаёт идентификатор ресурса, под которым изображение будет доступно в коде;[Sources]
дописываем оба файла ruvds.idf и ruvds.bmp, и удаляем из неё HelloWorldStr.uni: особенности [12] сборочных скриптов edk2 не позволяют иметь в одном проекте и BMP, и локализованные строки;[Protocols]
gEfiHiiDatabaseProtocolGuid ## CONSUMES
gEfiHiiImageProtocolGuid ## CONSUMES
gEfiHiiPackageListProtocolGuid ## CONSUMES
Наконец, в начало файла HelloWorld.c дописываем:
#include <Protocol/HiiDatabase.h>
#include <Protocol/HiiImage.h>
#include <Protocol/HiiPackageList.h>
А в начало UefiMain
— код для загрузки изображения:
EFI_HII_DATABASE_PROTOCOL *HiiDatabase;
EFI_HII_IMAGE_PROTOCOL *HiiImage;
EFI_HII_PACKAGE_LIST_HEADER *PackageList;
EFI_HII_HANDLE HiiHandle;
EFI_IMAGE_OUTPUT Output;
efiStatus = gBS->LocateProtocol(&gEfiHiiDatabaseProtocolGuid, NULL, (VOID**)&HiiDatabase);
if (EFI_ERROR(efiStatus)) {
Print(L"Unable to locate HII Databasen");
return EFI_NOT_STARTED;
}
efiStatus = gBS->LocateProtocol(&gEfiHiiImageProtocolGuid, NULL, (VOID**)&HiiImage);
if (EFI_ERROR(efiStatus)) {
Print(L"Unable to locate HII Imagen");
return EFI_NOT_STARTED;
}
efiStatus = gBS->OpenProtocol(ImageHandle, &gEfiHiiPackageListProtocolGuid,
(VOID**)&PackageList, ImageHandle, NULL, EFI_OPEN_PROTOCOL_GET_PROTOCOL);
if (EFI_ERROR (efiStatus)) {
Print(L"HII Image Package not found in PE/COFF resource sectionn");
return efiStatus;
}
efiStatus = HiiDatabase->NewPackageList(HiiDatabase, PackageList, NULL, &HiiHandle);
if (EFI_ERROR(efiStatus)) {
Print(L"Unable to register HII Packagen");
return EFI_NOT_STARTED;
}
После получения указателя на GOP можно проинициализировать структуру Output
:
Output.Width = (UINT16)gop->Mode->Info->HorizontalResolution;
Output.Height = (UINT16)gop->Mode->Info->VerticalResolution;
Output.Image.Screen = gop;
UINTN logo_offset = 38;
И теперь внутри цикла, сразу после очистки экрана, выведем это изображение:
EFI_IMAGE_OUTPUT *pOutput = &Output;
HiiImage->DrawImageId(HiiImage, EFI_HII_DIRECT_TO_SCREEN, HiiHandle,
IMAGE_TOKEN(IMG_LOGO), &pOutput, (UINTN)tree_width_base - logo_offset, 0);
Blt
DrawImageId
шлёт изображение из ресурсов напрямую на экран; нам же для эффекта мигающего логотипа понадобится буфер в памяти, где мы будем плавно менять яркость пикселей перед отрисовкой. Для работы с памятью в начало файла надо добавить:
#include <Library/BaseMemoryLib.h>
Теперь удалим объявление структуры Output
и её инициализацию, и вместо этого создадим буфер:
EFI_IMAGE_INPUT Image;
EFI_PHYSICAL_ADDRESS Buffer;
efiStatus = HiiImage->GetImage(HiiImage, HiiHandle, IMAGE_TOKEN(IMG_LOGO), &Image);
if (EFI_ERROR(efiStatus)) {
Print(L"Unable to locate IMG_LOGOn");
return EFI_NOT_STARTED;
}
UINTN size = Image.Height * Image.Width * sizeof(EFI_GRAPHICS_OUTPUT_BLT_PIXEL);
efiStatus = gBS->AllocatePages(AllocateAnyPages,
EfiLoaderData, EFI_SIZE_TO_PAGES(size), &Buffer);
if (EFI_ERROR(efiStatus)) {
Print(L"Unable to allocate Buffern");
return EFI_NOT_STARTED;
}
EFI_GRAPHICS_OUTPUT_BLT_PIXEL *logo = (EFI_GRAPHICS_OUTPUT_BLT_PIXEL*)(UINTN)Buffer;
CopyMem(logo, Image.Bitmap, size);
В начале цикла нет надобности очищать весь экран: вместо этого вызовом Blt(EfiBltVideoFill)
очистим лишь ту часть, где рисуется спираль-ёлочка.
gop->Blt(gop, logo, EfiBltVideoFill, 0, 0,
(UINTN)(tree_width_base - 1.2 * tree_width_factor), Image.Height,
(UINTN)(2.4 * tree_width_factor),
gop->Mode->Info->VerticalResolution - Image.Height, 0);
Саму спираль сместим вниз на высоту логотипа:
UINTN y_disp = (UINTN)(y + z * .25) + Image.Height;
И в завершение художества — в конце цикла, перед задержкой, рассчитываем и отрисовываем вызовом Blt(EfiBltBufferToVideo)
плавно мигающий логотип:
for (UINTN i = 0; i < Image.Width * Image.Height; i++) {
if (Image.Bitmap[i].Blue > Image.Bitmap[i].Red) {
if (sin > 0) {
// blend with (0,0,0)
logo[i].Red = (UINT8)(Image.Bitmap[i].Red * (1 - sin));
logo[i].Green = (UINT8)(Image.Bitmap[i].Green * (1 - sin));
logo[i].Blue = (UINT8)(Image.Bitmap[i].Blue * (1 - sin));
}
else {
// blend with (175, 224, 250)
logo[i].Red = (UINT8)(175 - (175 - Image.Bitmap[i].Red) * (1 + sin));
logo[i].Green = (UINT8)(224 - (224 - Image.Bitmap[i].Green) * (1 + sin));
logo[i].Blue = (UINT8)(250 - (250 - Image.Bitmap[i].Blue) * (1 + sin));
}
}
}
gop->Blt(gop, logo, EfiBltBufferToVideo,
0, 0, (UINTN)tree_width_base - logo_offset, 0,
Image.Width, Image.Height, Image.Width * sizeof(EFI_GRAPHICS_OUTPUT_BLT_PIXEL));
Окончательный вариант кода лежит в репозитории в каталоге HelloWorld [13], а его работа показана на ролике в начале и конце статьи.
Когда UEFI-приложение отлажено под эмулятором, то его можно запустить вживую, без UEFI Shell — для этого надо взять флешку, отформатированную как FAT; положить HelloWorld.efi по пути EFIBOOTbootx64.efi
; в настройках BIOS отключить Secure Boot; и вуаля!
Автор:
oldadmin
Источник [14]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/visual-studio/366886
Ссылки в тексте:
[1] симпатичная графическая демка: https://habr.com/ru/company/ruvds/blog/535600/
[2] отмечали: https://habr.com/ru/company/ruvds/blog/536132/#comment_22508110
[3] MinimumLaw: https://habr.com/ru/users/minimumlaw/
[4] DarkTiger: https://habr.com/ru/users/darktiger/
[5] туториал: https://habr.com/ru/post/338264/
[6] шаблон среды: https://github.com/ProgrammingInUEFI/FW
[7] архимедова спираль: https://ru.wikipedia.org/wiki/%D0%90%D1%80%D1%85%D0%B8%D0%BC%D0%B5%D0%B4%D0%BE%D0%B2%D0%B0_%D1%81%D0%BF%D0%B8%D1%80%D0%B0%D0%BB%D1%8C
[8] Chris Fallin: https://github.com/cfallin
[9] исключили: https://bugzilla.tianocore.org/show_bug.cgi?id=1734
[10] Benjamin Kietzman: https://gist.github.com/bkietz/c00c61e73f847d3cf4454c8c4cb4ced0#file-sincos-asm
[11] улучшенную версию эмулятора: https://github.com/ionescu007/VisualUefi/tree/master/debugger
[12] особенности: https://bugzilla.tianocore.org/show_bug.cgi?id=3517
[13] HelloWorld: https://github.com/tyomitch/uefi/tree/master/HelloWorld
[14] Источник: https://habr.com/ru/post/571624/?utm_source=habrahabr&utm_medium=rss&utm_campaign=571624
Нажмите здесь для печати.