Разработка 64-битного графического UEFI-приложения в Visual Studio

от автора

Зимой в блоге RUVDS было несколько статей о написании загрузчиков для «классического» BIOS, в т.ч. симпатичная графическая демка, целиком помещающаяся в загрузочном секторе и работающая в VGA-режиме 320×200. Комментаторы справедливо отмечали, что в наступившем 2021 г. нет смысла осваивать это лютое легаси;

а вот статей про “Hello, World” на UEFI да с графикой действительно не хватает. Больше того — я таких вообще не припомню.» (MinimumLaw)

Под катом мы пошагово перепишем ту бутсекторную демку под UEFI, и она будет работать в полноцветном видеорежиме с высоким разрешением. С другой стороны, вместо 512 байт она будет занимать несколько десятков КБ.

▍ 1. Подготовка среды

Четыре года назад DarkTiger постил туториал о разработке под UEFI в Visual Studio, и даже опубликовал шаблон среды, позволяющий начать разработку, не ломая голову над настройками 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

Список изменений в шаблоне

  1. Копия edk2 заменена ссылкой (git submodule) на официальный репозиторий;
  2. Добавлены бинарники Python 3.5, и сборочные скрипты внутри NT32.vcxproj задают переменную PYTHON_HOME, нужную для edk2;
  3. Захардкоженный путь C:\FW\ в файлах vcxproj заменён на $(SolutionDir)\..\;
  4. Скрипту edksetup.bat вместо ставшего неактуальным параметра --nt32 передаются Rebuild VS2019: первый указывает на необходимость скомпилировать edk2\BaseTools\Bin\Win32 (несмотря на название параметра, компиляция выполняется инкрементально); второй — на используемый тулчейн.

Конфигурация проекта по-прежнему называется “Win32”; на целевую платформу, фактически используемую при компиляции, это никак не влияет.

Перед сборкой нужно поменять внутри edk2\BaseTools\Conf\target.template значения TARGET_ARCH на X64 и TOOL_CHAIN_TAG на VS2019; после этого можно открывать VS\NT32.sln, жать F5, и всё скомпилируется и запустится. Чтобы удостовериться, что среда полноценно работает, введите в UEFI Shell команду fs0:HelloWorld.efi:

Модуль HelloWorld мы и возьмём за основу для нашей демки.

▍ 2. Тригонометрия

Отрисовываемая демкой линия — это вертикально растянутая архимедова спираль, и для расчёта координат её точек Chris Fallin использовал инструкции fcos и fsin. Увы, но тригонометрические функции в Си не встроены: они относятся к стандартной библиотеке, а её из edk2 исключили. Нет в MSVC и инлайн-ассемблера для x64, так что функцию SinCos придётся реализовывать в отдельном ассемблерном файле, следуя примеру Benjamin Kietzman. Поместите его файл SinCos.asm рядом с исходником HelloWorld.c в каталоге edk2\MdeModulePkg\Application\HelloWorld, а в файл проекта 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. Обычно его экспортирует стандартная библиотека, но нам его приходится объявлять вручную.

▍ 3. Пиксельная графика

Стандартный для 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 GOP\n");         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. Чтобы тестировать код, требующий прямого доступа к видеопамяти, можно использовать улучшенную версию эмулятора, собранную 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:…\edk2\Build\EmulatorX64\DEBUG_VS2019\X64,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:

Сразу заметны три проблемы, унаследованные из бутсекторной демки:

  1. Экран вначале очищается вызовом gST->ConOut->ClearScreen, а затем на нём по одному зажигаются пиксели спирали-ёлочки. Вызываемое этим мерцание заметно даже на гифке. Чтобы от него избавиться, ёлочку надо отрисовывать в невидимый буфер, а затем вызовом Blt отправлять на экран одним целым. Мы же объявим мерцание воображаемых гирлянд тёплой и ламповой фичей демки, и оставим его как есть.
  2. Единственный способ выйти из демки — перезагрузка. Для загрузчика в режиме Legacy BIOS это нормально (куда ж из него выходить?), но из UEFI-приложения хотелось бы иметь возможность выйти обратно в UEFI Shell. В конце цикла, после задержки — проверим, была ли нажата какая-нибудь клавиша:
        EFI_INPUT_KEY Key;     efiStatus = gST->ConIn->ReadKeyStroke(gST->ConIn, &Key);     if (!EFI_ERROR(efiStatus)) {         return efiStatus;     }
  3. В режиме 320×200 одиночные пиксели двух цветов выглядят нормально; в высоком разрешении — очень бледно: и в прямом, и в переносном смысле. Во-первых, сделаем их жирнее — каждую точку спирали будем отрисовывать «плюсиком» из пяти соседних пикселей; во-вторых, воспользуемся полноцветностью GOP, и будем менять цвет от зелёного к красному плавным градиентом, повторяющимся пятьдесят раз на протяжении спирали.
        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;     }

▍ 4. Графические ресурсы

Изображение, выводимое рядом со спиралью-ёлочкой в бутсекторной демке, захардкожено прямо в ассемблерном исходнике простынью из определений db. Это неудобно: как такое изображение редактировать? Гораздо удобнее работать с BMP-файлом. Такую возможность даёт HII Database (Human Interface Infrastructure), вскользь упомянутая в туториале от DarkTiger как хранилище шрифтов, доступных UEFI-приложениям. Изображения, как и шрифты, включаются в PE-образ приложения как ресурсы. Для того, чтобы добавить в ресурсы BMP-изображение, нужно несколько неочевидных шагов:

  1. Кладём файл ruvds.bmp в каталог проекта;
  2. Там же создаём файл ruvds.idf из одной строчки #image IMG_LOGO ruvds.bmp — она задаёт идентификатор ресурса, под которым изображение будет доступно в коде;
  3. В файле HelloWorld.inf в секцию [Sources] дописываем оба файла ruvds.idf и ruvds.bmp, и удаляем из неё HelloWorldStr.uni: особенности сборочных скриптов edk2 не позволяют иметь в одном проекте и BMP, и локализованные строки;
  4. В конец файла HelloWorld.inf дописываем новую секцию:
    [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 Database\n");         return EFI_NOT_STARTED;     }      efiStatus = gBS->LocateProtocol(&gEfiHiiImageProtocolGuid, NULL, (VOID**)&HiiImage);     if (EFI_ERROR(efiStatus)) {         Print(L"Unable to locate HII Image\n");         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 section\n");         return efiStatus;     }      efiStatus = HiiDatabase->NewPackageList(HiiDatabase, PackageList, NULL, &HiiHandle);     if (EFI_ERROR(efiStatus)) {         Print(L"Unable to register HII Package\n");         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);

▍ 5. Работа с 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_LOGO\n");         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 Buffer\n");         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, а его работа показана на ролике в начале и конце статьи.

Когда UEFI-приложение отлажено под эмулятором, то его можно запустить вживую, без UEFI Shell — для этого надо взять флешку, отформатированную как FAT; положить HelloWorld.efi по пути \EFI\BOOT\bootx64.efi; в настройках BIOS отключить Secure Boot; и вуаля!


ссылка на оригинал статьи https://habr.com/ru/articles/571624/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *