Как написать uefi загрузчик

Всем привет. В рамках проекта от компании Acronis со студентами Университета Иннополис (подробнее о проекте мы уже описали это тут и тут) мы изучали последовательность загрузки операционной системы Windows. Появилась идея исполнять логику даже до загрузки самой ОС. Следовательно, мы попробовали написать что-нибудь для общего развития, для плавного погружения в UEFI. В этой статье мы пройдем по теории и попрактикуемся с чтением и записью на диск в pre-OS среде.

cover

В компании Acronis команд, которые занимаются UEFI, не так много, поэтому я решил разобраться в вопросе самостоятельно. К тому же есть проверенный способ получить огромное количество точных советов совершенно бесплатно и свободно — просто начать делать что-либо и выложить это в интернет. Поэтому комментарии и рекомендации под этим постом очень приветствуются! Вторая цель данного поста собрать небольшой дайджест статей о UEFI и помочь двигаться в этом направлении.

Полезные ссылки

Для начала хочу перечислить список источников, которые мне очень помогли. Возможно вам они тоже помогут и ответят на ваши вопросы.

  • В первую очередь это UEFI and EDK II Learning and Development от tianocore. Прекрасно структурированный и иллюстрированный курс, который поможет понять что же происходит в момент загрузки и что такое UEFI. Если вы ищите точную теоретическую информацию по теме, вам туда. Если хочется поскорее перейти к написанию UEFI драйверов, то сразу в Lesson 3.
  • Статьи на хабре раз, два и три. Автору низкий поклон. Это отличное практическое руководство без лишних сложностей для начинающих. Частично в данной статье я буду цитировать эти шедевры, хоть и с небольшими изменениями. Без этих публикаций, было бы значительно тяжелее начать.
  • Для продолжающих рекомендую эту статью и другие этого же автора.
  • Так как мы планируем писать драйвер, очень поможет официальный гайдлайн по написанию драйвера. Наиболее правильные советы будут именно там.
  • Ну и на крайний случай спецификация UEFI.

Немного теории

Хочу напомнить требования и цели проекта Active Restore. Мы планируем приоритизировать файлы в системе для более эффективного восстановления. Для этого нужно запуститься на максимально раннем этапе загрузки ОС. Для понимания наших возможностей в мире UEFI стоит немного углубиться в теорию о том как проходит цикл загрузки. Информация для этой части полностью взята из этого источника, который я постараюсь популярно пересказать.

UEFI

UEFI или Unified Extensible Firmware Interface стал эволюцией Legacy BIOS. В модели UEFI тоже есть базовая система ввода-вывода для взаимодействия с железом, хотя процесс загрузки системы и стал отличаться. UEFI использует GPT (Guid partition table). GPT тесно связана со спецификацией и является более продвинутой моделью для хранения информации о разделах диска. Изменился процесс, но задачи остались прежними: инициализация устройств ввода-вывода и передача управления в код операционной системы. UEFI не только заменяет бóльшую часть функций BIOS, но также предоставляет широкий спектр возможности для разработки в pre-OS среде. Хорошее сравнение Legacy BIOS и UEFI есть тут.

arch

В данном представлении BIOS это компонент, обеспечивающий непосредственное общение с железом и является firmware. UEFI это унификация интерфейса железа для операционной системы, что существенно облегчает жизнь разработчикам.

В мире UEFI мы можем разрабатывать драйвера или приложения. Есть специальный подтип приложений — загрузчики. Разница лишь в том, что эти приложения не завершаются привычным нам образом. Завершаются они вызовом функции ExitBootServices() и передают управление в операционную систему. Чтобы принять решение какой же драйвер нужен вам, рекомендую заглянуть сюда, чтобы расширить понимание о протоколах и рекомендациях по их использованию.

Dev kits

Небольшой список того, что мы будем использовать в нашей практике:

  • EDKII (Extensible Firmware Interface Development Kit) — является свободно распространяемым проектом для разработки UEFI приложений и драйверов, которую и мы будем использовать, по началу не сильно в неё углубляясь.
  • VisualUEFI — проект облегчающий разработку в Visual Studio. Больше не нужно заморачиваться с .inf файлами и ковыряться в 100500 скриптах на Python. Все это уже сделано за вас. Внутри можно найти QEMU для запуска нашего кода. В проекте представлены примеры приложения и драйверов.
  • Coreboot — комплексный проект для firmware. Его задача — помочь разработать решение для старта железа и передачи управления в payload (например UEFI или GRUB), который в свою очередь загрузит операционную систему. В данной статье мы не будем затрагивать coreboot. Оставим его для будущих экспериментов, когда набью руку с EDKII. Возможно правильным вектором развития будет Coreboot + Tianocore UEFI + Windows 7 x64.

Последовательность загрузки

Коротко разберем через какие стадии проходит наша машина, прежде чем мы видим заветный логотип операционной системы. Для этого рассмотрим следующую диаграмму:

boot_seq
Ссылка

Процесс с момента нажатия на кнопку питания на корпусе и до полной готовности UEFI интерфейса называется Platform Initialization и делится он на несколько фаз:

  • Security (SEC) — зависит от платформы и процессора, обычно реализована ассемблерными командами, проводит первоначальную инициализацию временной памяти, проверку остальной части платформы на безопасность различными способами.
  • Pre EFI Initialization (PEI) — в данной фазе уже начинается работа EFI кода, главная задача — загрузка DXE Foundation который будет стартовать DXE драйверов на следующей фазе. На самом деле, тут происходит еще очень много всего, но то, что мы планируем разрабатывать, сюда не пролезет, так что двигаемся дальше.
  • Driver Execution Environment (DXE) — на данном этапе начинают стартовать драйвера. Наиболее важная для нас фаза, потому, что наш драйвер тоже будет запущен тут. Данная среда исполнения драйверов и является основным преимуществом над Legacy BIOS. Тут код начинает исполняться параллельно. DXE ведет себя на манер операционной системы. Это позволяет различным компаниям имплементировать свои драйвера. DXE Foundation, развернутый на предыдущей фазе, поочередно находит драйвера, библиотеки и приложения, разворачивает их памяти и исполняет.
  • После этой фазы эстафету принимает Boot Device Selection (BDS). Вы наверняка лично видели данную фазу. Тут происходит выбор на каком устройстве искать приложение — загрузчик операционной системы. После выбора начинается переход к операционной системе. DXE boot драйвера начинают выгружаться из памяти. Загрузчик операционной системы наоборот загружается в память с помощью блочного протокола ввода — вывода BLOCK_IO. Здесь не все DXE драйвера завершают свою работу. Существуют так называемые runtime драйвера. Им придется на понятной для загруженной операционной системе нотации разметить память, которую они занимают. Иными словами виртуализировать свои адреса в адресное пространство Windows, когда произойдет вызов функции SetVirtualAddressMap(). Как только среда будет готова, “Main” функция ядра ОС начнет исполнение, а фаза EFI завершится вызовом ExitBootServices(). Контроль полностью передан в операционную систему. Дальше Windows будет решать какие и откуда загрузить дайвера, как читать и писать на диск и что за файловую систему использовать. Картинка обобщающая вышеуказанную последовательность:

image
Ссылка

Классный рассказ о этапах загрузки есть тут.

Подготовка проекта

Пришло время поставить перед собой простую задачу. Мы можем загрузить наш драйвер в DXE фазе, открыть файл на диске и записать в него какие — нибудь данные. Задача достаточно простая, чтобы потренироваться.

Как я уже упоминал мы воспользуемся проектом VisualUEFI, однако рекомендую также попробовать способы описанные тут, хотя бы потому, что использовать дебагер легче в описанном по ссылке способе.

Допускаю, что у вас уже есть Visual Studio. В моем случае у меня Visual Studio 2019. Для начала клонируем себе проект VisualUEFI:

git clone --recurse-submodules -j8 https://github.com/ionescu007/VisualUefi.git

Нам понадобится NASM (https://www.nasm.us/pub/nasm/releasebuilds/2.15.02/win64/). Переходим и скачиваем. На момент написания статьи актуальной версией является 2.15.02. После установки убедитесь, что в переменных средах у вас есть NASM_PREFIX, который указывает на папку, в которую был установлен NASM. В моем случае это C:Program FilesNASM.

NASM_PREFIX

Соберем EDKII. Для этого открываем EDK-II.sln из VisualUefiEDK-II, и просто жмем build на решении. Все проекты в решении должны успешно собраться, и можно переходить к уже готовым примерам. Открываем samples.sln из VisualUefisamples. Жмем build на приложении и драйвере, после чего можно запускать QEMU простым нажатием F5.

Shell

Проверяем наш UefiDriver и UefiApplication, именно так называются примеры в решении samples.sln.

Shell> fs1:
FS1:> load UefiDriver.efi

load

Отлично, драйвер не только собрался, но и успешно загрузился. Выполнив команду drivers, мы даже увидим его в списке.

drivers

Если бы в коде мы не возвращали EFI_ACCESS_DENIED в функции UefiUnload, мы бы даже смогли выгрузить наш драйвер, выполнив команду:

FS1:> unload BA

Теперь вызовем наше приложение:

FS1:> UefiApplication.efi

app

Написание кода

Рассмотрим код предоставленного нам драйвера. Все начинается с функции UefiMain, которая находится в файле drvmain.c. Мы бы могли назвать точку входа и другим именем, если бы писали драйвер “с нуля”, указать это можно было бы в .inf файле.

EFI_STATUS
EFIAPI
UefiUnload (
    IN EFI_HANDLE ImageHandle
    )
{
    //
    // Do not allow unload
    //
    return EFI_ACCESS_DENIED;
}

EFI_STATUS
EFIAPI
UefiMain (
    IN EFI_HANDLE ImageHandle,
    IN EFI_SYSTEM_TABLE *SystemTable
    )
{
    EFI_STATUS efiStatus;

    //
    // Install required driver binding components
    //
    efiStatus = EfiLibInstallDriverBindingComponentName2(ImageHandle,
                                                         SystemTable,
                                                         &gDriverBindingProtocol,
                                                         ImageHandle,
                                                         &gComponentNameProtocol,
                                                         &gComponentName2Protocol);
    return efiStatus;
}

В проекте от нас не требуют регистрировать Unload функцию, так как VisualUEFI это и так уже делает “под капотом”, нужно просто её объявить. В примере она в этом же файле и называется UefiUnload. В этой функции мы можем написать код, который освободит все занятые нами ресурсы, так как она будет вызвана при выгрузке драйвера. Регистрация Unload функции в проекте VisualUEFI происходит в файле DriverEntryPoint.c, в функции _ModuleEntryPoint.

// _DriverUnloadHandler manages to call UefiUnload
Status = gBS->HandleProtocol (
                    ImageHandle,
                    &gEfiLoadedImageProtocolGuid,
                    (VOID **)&LoadedImage
              );
ASSERT_EFI_ERROR (Status);
LoadedImage->Unload = _DriverUnloadHandler;

В нашем примере, в функции UefiMain, происходит вызов функции EfiLibInstallDriverBindingComponentName2, которая регистрирует имя нашего драйвера и Driver Binding Protocol. Согласно модели драйверов UEFI, все драйвера устройств должны регистрировать этот протокол для предоставления контроллеру функций Support, Start, Stop. Функция Support отвечает, может ли наш драйвер работать с данным контроллером. Если да, то вызывается функция Start. Подробнее об этом хорошо описано в спецификации (раздел Protocols — UEFI Driver Model). В нашем примере функции Support, Start и Stop устанавливают наш кастомный протокол. Его реализация в файле drvpnp.c:

//
// EFI Driver Binding Protocol
//
EFI_DRIVER_BINDING_PROTOCOL gDriverBindingProtocol =
{
    SampleDriverSupported,
    SampleDriverStart,
    SampleDriverStop,
    10,
    NULL,
    NULL
};

…

//
// Install our custom protocol on top of a new device handle
//
efiStatus = gBS->InstallMultipleProtocolInterfaces(&deviceExtension->DeviceHandle,
                                                       &gEfiSampleDriverProtocolGuid,
                                                       &deviceExtension->DeviceProtocol,
                                                       NULL);

//
// Bind the PCI I/O protocol between our new device handle and the controller
//
efiStatus = gBS->OpenProtocol(Controller,
                                  &gEfiPciIoProtocolGuid,
                                  (VOID**)&childPciIo,
                                  This->DriverBindingHandle,
                                  deviceExtension->DeviceHandle,
                                  EFI_OPEN_PROTOCOL_BY_CHILD_CONTROLLER);

Фукнция EfiLibInstallDriverBindingComponentName2 реализована в файле UefiDriverModel.c, и, на самом деле, очень простая. Она вызывает InstallMultipleProtocolInterfaces из Boot Services (см. Спецификацию стр 210). Данная функция связывает handle (в нашем случае ImageHandle, который мы получили на точке входа) и протокол.

// install component name and binding
Status = gBS->InstallMultipleProtocolInterfaces (
                       &DriverBinding->DriverBindingHandle,
                       &gEfiDriverBindingProtocolGuid, DriverBinding,
                       &gEfiComponentNameProtocolGuid, ComponentName,
                       &gEfiComponentName2ProtocolGuid, ComponentName2,
                       NULL
                       );

Соответственно, можно, и нужно, в момент выгрузки драйвера, удалить установленные компоненты. Мы сделаем это в нашей функции Unload. Теперь наш драйвер можно будет выгружать по команде unload , или перед передачей управления в операционную систему.

EFI_STATUS
EFIAPI
UefiUnload (
    IN EFI_HANDLE ImageHandle
    )
{
  gBS->UninstallMultipleProtocolInterfaces(
    ImageHandle,
    &gEfiDriverBindingProtocolGuid, &gDriverBindingProtocol,
    &gEfiComponentNameProtocolGuid, &gComponentNameProtocol,
    &gEfiComponentName2ProtocolGuid, &gComponentName2Protocol,
    NULL
  );
    //
    // Changed from access denied in order to unload in boot
    //
    return EFI_SUCCESS;
}

Как вы могли заметить, в нашем коде мы взаимодействуем с UEFI через глобальное поле gBS (global Boot Services). Также, существует gRT (global Runtime Services), а вместе они являются частью структуры System Table. Источник.

gST = *SystemTable; 
gBS = gST->BootServices; 
gRT = gST->RuntimeServices;

Для работы с файлами нам понадобится Simple File System Protocol (см. Спецификацию стр 504). Вызвав функцию LocateProtocol, можно получить на него указатель, хотя более правильный способ перечислить все handles на устройства файловой системы с помощью функции LocateHandleBuffer, и, перебрав все протоколы Simple File System, выбрать подходящий, который позволит нам писать и читать в файл. Пример такого кода тут. А мы же воспользуемся способом проще. У протокола есть всего одна функция, которая позволит нам открыть том.

EFI_STATUS
OpenVolume(
  OUT EFI_FILE_PROTOCOL** Volume
)
{
  EFI_SIMPLE_FILE_SYSTEM_PROTOCOL* fsProto = NULL;
  EFI_STATUS status;
  *Volume = NULL;

  // get file system protocol
  status = gBS->LocateProtocol(
    &gEfiSimpleFileSystemProtocolGuid,
    NULL,
    (VOID**)&fsProto
  );

  if (EFI_ERROR(status))
  {
    return status;
  }

  status = fsProto->OpenVolume(
    fsProto,
    Volume
  );

  return status;
}

Далее, нам необходимо уметь создавать файл и закрывать его. Воспользуемся EFI_FILE_PROTOCOL, в котором есть функции для работы с файловой системой (см. Спецификацию стр 506).

EFI_STATUS
OpenFile(
  IN  EFI_FILE_PROTOCOL* Volume,
  OUT EFI_FILE_PROTOCOL** File,
  IN  CHAR16* Path
)
{
  EFI_STATUS status;
  *File = NULL;

  //  from root file we open file specified by path
  status = Volume->Open(
    Volume,
    File,
    Path,
    EFI_FILE_MODE_CREATE |
    EFI_FILE_MODE_WRITE |
    EFI_FILE_MODE_READ,
    0
  );

  return status;
}

EFI_STATUS
CloseFile(
  IN EFI_FILE_PROTOCOL* File
)
{
  //  flush unwritten data
  File->Flush(File);
  //  close file
  File->Close(File);

  return EFI_SUCCESS;
}

Для записи в файл нам придется вручную двигать каретку. Для этого будем спрашивать размер файла с помощью функции GetInfo.

EFI_STATUS
WriteDataToFile(
  IN VOID* Buffer,
  IN UINTN BufferSize,
  IN EFI_FILE_PROTOCOL* File
)
{
  UINTN infoBufferSize = 0;
  EFI_FILE_INFO* fileInfo = NULL;

  //  retrieve file info to know it size
  EFI_STATUS status = File->GetInfo(
    File,
    &gEfiFileInfoGuid,
    &infoBufferSize,
    (VOID*)fileInfo
  );

  if (EFI_BUFFER_TOO_SMALL != status)
  {
    return status;
  }

  fileInfo = AllocatePool(infoBufferSize);

  if (NULL == fileInfo)
  {
    status = EFI_OUT_OF_RESOURCES;
    return status;
  }

  //    we need to know file size
  status = File->GetInfo(
    File,
    &gEfiFileInfoGuid,
    &infoBufferSize,
    (VOID*)fileInfo
  );

  if (EFI_ERROR(status))
  {
    goto FINALLY;
  }

  //    we move carriage to the end of the file
  status = File->SetPosition(
    File,
    fileInfo->FileSize
  );

  if (EFI_ERROR(status))
  {
    goto FINALLY;
  }

  //    write buffer
  status = File->Write(
    File,
    &BufferSize,
    Buffer
  );

  if (EFI_ERROR(status))
  {
    goto FINALLY;
  }

  //    flush data
  status = File->Flush(File);

FINALLY:

  if (NULL != fileInfo)
  {
    FreePool(fileInfo);
  }

  return status;
}

Вызываем наши функции и пишем случайные данные в наш файл:

EFI_STATUS
WriteToFile(
  VOID
)
{
  CHAR16 path[] = L"\example.txt";
  EFI_FILE_PROTOCOL* file = NULL;
  EFI_FILE_PROTOCOL* volume = NULL;
  CHAR16 something[] = L"Hello from UEFI driver";

  //
  //  Open file
  //
  EFI_STATUS status = OpenVolume(&volume);

  if (EFI_ERROR(status))
  {
    return status;
  }

  status = OpenFile(volume, &file, path);

  if (EFI_ERROR(status))
  {
    CloseFile(volume);
    return status;
  }

  status = WriteDataToFile(something, sizeof(something), file);

  CloseFile(file);
  CloseFile(volume);

  return status;
}

Есть альтернативный способ выполнить нашу задачу. В проекте VisualUEFI уже реализовано то, что мы написали выше. Мы можем просто подключить заголовочный файл ShellLib.h и вызвать в самом начале функцию ShellInitialize. Все необходимые протоколы для работы с файловой системой будут открыты, а функции ShellOpenFileByName, ShellWrite и ShellRead реализованы почти так же, как и у нас.

#include <Library/ShellLib.h>

EFI_STATUS
WriteToFile2(
  VOID
)
{
  SHELL_FILE_HANDLE fileHandle = NULL;
  CHAR16 path[] = L"fs1:\example2.txt";
  CHAR16 something[] = L"Hello from UEFI driver";
  UINTN writeSize = sizeof(something);

  EFI_STATUS status = ShellInitialize();

  if (EFI_ERROR(status))
  {
    return status;
  }

  status = ShellOpenFileByName(path,
    &fileHandle,
    EFI_FILE_MODE_CREATE |
    EFI_FILE_MODE_WRITE |
    EFI_FILE_MODE_READ,
    0);

  if (EFI_ERROR(status))
  {
    return status;
  }

  status = ShellWriteFile(fileHandle, &writeSize, something);

  ShellCloseFile(&fileHandle);
  return status;
}

Результат:

result

→ Код этого примера на github

Если мы хотим перейти в VMWare, то наиболее правильным будет модификация firmware с помощью UEFITool. Например тут демонстрируется как добавляют NTFS драйвер в UEFI.

Выводы

Усложнить идею нашего драйвера и ближе подвести его под требования проекта Active Restore можно следующим образом: открыть протокол BLOCK_IO, заменить функции чтения на диск нашими функциями, которые запишут данные, читаемые с диска в лог и затем вызовут оригинальные функции. Сделать это можно следующим образом:

// just pseudo code
...

// open protocol to replace callbacks
gBS->OpenProtocol(
      Controller,
      Guid,
      (VOID**)&protocol,
      DriverBindingHandle,
      Controller,
      EFI_OPEN_PROTOCOL_GET_PROTOCOL
    );

// raise Task Priority Level to max avaliable
gBS->RaiseTPL(TPL_NOTIFY);

VOID** protocolBase = EFI_FIELD_BY_OFFSET(VOID**, FilterContainer, 0);
VOID** oldCallback = EFI_FIELD_BY_OFFSET(VOID**, *protocolBase, oldCallbackOffset);
VOID** originalCallback = EFI_FIELD_BY_OFFSET(VOID**, FilterContainer, originalCallbackOffset);

//  yes, I know that it is not super obvious
//  but if first and third is equal (placeholder and function)
//  then the first one is not the function it is offset!
//  and function itself is by offset of third one
if ((UINTN) newCallback == originalCallbackOffset)
{
  newCallback = *originalCallback;
}

PRINT_DEBUG(DEBUG_INFO, L"[UefiMonitor] 0x%x -> 0x%xn", *oldCallback, newCallback);

//saving original functions
*originalCallback = *oldCallback;
//replacing them by filter function
*oldCallback = newCallback;

// restore TPL
gBS->RestoreTPL(oldTpl);

Нужно будет не забыть подписаться на ExitBootServices(), чтобы вернуть указатели на место. После того, как фильтр файловой системы в Windows будет готов, минифильтр продолжит логировать чтение с диска.

// event on exit
gBS->CreateEvent(
      EVT_SIGNAL_EXIT_BOOT_SERVICES,
      TPL_NOTIFY,
      ExitBootServicesNotifyCallback,
      NULL,
      &mExitBootServicesEvent
    );

Но это это уже идеи для будущих статей. Спасибо за внимание.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Считаете ли вы что разработка в UEFI (и в целом около embedded разработка) — нечто сложное и недосягаемое для вас?


16.67%
Да, это слишком сложно для меня
9


46.3%
Всегда можно найти материалы, которые объяснят просто о сложном
25

Проголосовали 54 пользователя.

Воздержались 8 пользователей.

uefi-elf-bootloader

This repository contains a simple UEFI ELF bootloader which loads a simple demonstration kernel. It provides an extremely basic example implementation of a UEFI bootloader for a bare-metal x86-64 system, though this example should be portable to other architectures.

The aim of this repository is to serve as a basic teachable example of how to implement a UEFI bootloader.

Build instructions

This bootloader assumes a GCC cross-compiler toolchain targeting the bare-metal x86_64-elf architecture. Instructions for building and obtaining a valid cross-compiler toolchain can be found here.

This bootloader can be built simply by running make within the src directory. This will create the build/kernel.img file, which is a bootable disk image containing the bootloader loading a demonstration kernel. There is a run script within the root directory containing a script for testing the bootloader/kernel combination using QEMU.

Build dependencies

  • GNU Make
  • GNU EFI
  • An x86_64-elf-gcc cross-compiler toolchain present in PATH

Project structure

This project is broken down into two distinct components: The bootloader, and the example kernel. These can be found in the src/bootloder and src/kernel directories respectively. These can be built and tested individually using the makefiles within their individual directories. Running the makefile within the top level src directory will build the entire project.

Bootloader

The bootloader component of this repository, contained within the src/bootloader directory, contains the basic implementation of a UEFI ELF bootloader for the x86-64 platform. The bootloader is hardcoded to load and execute a bare-metal x86-64 application located at /kernel.elf on the boot media.
This path can be modifid from within the src/bootloader/src/include/bootloader.h file by modifying the KERNEL_INCLUDE_PATH preprocessor directive.

The bootloader will output debugging information over the system’s serial port, if present. Otherwise VGA output will be used.

The bootloader passes a Kernel_Boot_Info struct to the loaded kernel containing basic system information, such as the memory map. This struct is defined within the src/bootloader/src/include/bootloader.h header file. This implementation is not tied to any specific architecture.

The bootloader will open the Graphics Output Protocol and Serial Protocol. A routine has been provided for drawing a test screen to demonstrate that the graphics output protocol has been loaded correctly. This can be toggled by setting the DRAW_TEST_SCREEN preprocessor directive at the top of the src/bootloader/src/main.c file.

Kernel

This repository contains a minimal x86-64 kernel for testing purposes. This is located within the src/kernel directory. It contains a basic UART implementation suitable for testing that the kernel has been correctly loaded.

Feedback

Feel free to direct any questions or feedback to me directly at ajxs [at] panoptic.online

Difficulty level
Difficulty 2.png
Medium

In this tutorial, developers will create a hard drive or ISO image containing a bare bones UEFI application for the x86-64 platform.

It is recommended to have read and fully understood the Bare Bones tutorial first. The UEFI page provides some background to the UEFI boot process and should also be consulted first.

This tutorial uses the header files and GUID definitions from the GNU-EFI project, but does not use the gnu-efi build system, but rather the MinGW-w64 or LLVM/Clang toolchain.

Contents

  • 1 Prerequisites
  • 2 Testing the emulator
  • 3 Preparing the files
    • 3.1 hello.c
    • 3.2 gnu-efi/lib/data.c
    • 3.3 gnu-efi/lib/lib.h
  • 4 Building
    • 4.1 Under LLVM/clang
  • 5 Creating the FAT image
    • 5.1 Running as a USB stick image
    • 5.2 Creating and running the HD image
    • 5.3 Creating and running the CD image
  • 6 What to do next?
  • 7 Common problems
  • 8 See also

Prerequisites

Developers will need a GCC Cross-Compiler or Clang targeting the x86_64-w64-mingw32 target (for PE output), and the gnu-efi package (for UEFI headers). Most Linux distros provide cross-compilers for this target, so it’s usually not necessary to build it yourself. This example does not link against GNU-EFI or follow its build process; only the headers are used.

To build the EFI filesystem image, developers can use MTools or mkgpt to create a hard disk image. To build a CD image, xorriso (in mkisofs emulation mode) will be needed. To run under an emulator, it is best to use qemu-system-x86_64 coupled with the x64 OVMF firmware.

Under an apt-based system (e.g. Debian/Ubuntu), developers can run:

sudo apt-get install qemu ovmf gnu-efi binutils-mingw-w64 gcc-mingw-w64 xorriso mtools

To install mkgpt you can run these commands:

git clone https://github.com/jncronin/mkgpt.git
cd mkgpt
automake --add-missing
autoreconf
./configure
make
sudo make install

Testing the emulator

Now is a good time to check the emulator is working successfully with the OVMF firmware.

qemu-system-x86_64 -L OVMF_dir/ -pflash OVMF.fd

should launch qemu and launch a UEFI shell prompt.

Preparing the files

hello.c

Next, create a file with the following:

#include <efi.h>
#include <efilib.h>
 
EFI_STATUS efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
{
    EFI_STATUS Status;
    EFI_INPUT_KEY Key;
 
    /* Store the system table for future use in other functions */
    ST = SystemTable;
 
    /* Say hi */
    Status = ST->ConOut->OutputString(ST->ConOut, L"Hello Worldrn"); // EFI Applications use Unicode and CRLF, a la Windows
    if (EFI_ERROR(Status))
        return Status;
 
    /* Now wait for a keystroke before continuing, otherwise your
       message will flash off the screen before you see it.
 
       First, we need to empty the console input buffer to flush
       out any keystrokes entered before this point */
    Status = ST->ConIn->Reset(ST->ConIn, FALSE);
    if (EFI_ERROR(Status))
        return Status;
 
    /* Now wait until a key becomes available.  This is a simple
       polling implementation.  You could try and use the WaitForKey
       event instead if you like */
    while ((Status = ST->ConIn->ReadKeyStroke(ST->ConIn, &Key)) == EFI_NOT_READY) ;
 
    return Status;
}

gnu-efi/lib/data.c

Developers will also need bring in the data.c file from the gnu-efi distribution, as this contains many predefined GUIDs for the various UEFI services. To avoid bloat and unnecessary dependencies on the rest of gnu-efi, it will need to be edited to remove the references to ‘LibStubStriCmp’, ‘LibStubMetaiMatch’, and ‘LibStubStrLwrUpr’ (simply set all the members of the LibStubUnicodeInterface structure be NULL).

gnu-efi/lib/lib.h

data.c includes this file. It must be copied as-is to the source directory.

Building

To build, use the cross-compiler:

# compile: (flags before -o become CFLAGS in the Makefile)
x86_64-w64-mingw32-gcc -ffreestanding -Ipath/to/gnu-efi/inc -Ipath/to/gnu-efi/inc/x86_64 -Ipath/to/gnu-efi/inc/protocol -c -o hello.o hello.c
x86_64-w64-mingw32-gcc -ffreestanding -Ipath/to/gnu-efi/inc -Ipath/to/gnu-efi/inc/x86_64 -Ipath/to/gnu-efi/inc/protocol -c -o data.o path/to/gnu-efi/lib/data.c
# link: (flags before -o become LDFLAGS in the Makefile)
x86_64-w64-mingw32-gcc -nostdlib -Wl,-dll -shared -Wl,--subsystem,10 -e efi_main -o BOOTX64.EFI hello.o data.o

Note here that ‘—subsystem 10’ specifies an EFI application for ld.

Under LLVM/clang

The build sequence under LLVM/clang is essentially the same, although there is the advantage of having all targets installed by default:

CFLAGS='-target x86_64-unknown-windows 
        -ffreestanding 
        -fshort-wchar 
        -mno-red-zone 
        -Ipath/to/gnu-efi/inc -Ipath/to/gnu-efi/inc/x86_64 -Ipath/to/gnu-efi/inc/protocol'
LDFLAGS='-target x86_64-unknown-windows 
        -nostdlib 
        -Wl,-entry:efi_main 
        -Wl,-subsystem:efi_application 
        -fuse-ld=lld-link'
clang $CFLAGS -c -o hello.o hello.c
clang $CFLAGS -c -o data.o path/to/gnu-efi/lib/data.c
clang $LDFLAGS -o BOOTX64.EFI hello.o data.o

Passing ‘—target x86_64-unknown-windows’ to clang tells it to compile for x86_64 «Windows». This is quite not the same as 64-bit UEFI PE yet, but as before the «freestanding» part makes it a good kernel image. An example of this toolchain is found in the c-efi project.

Note the ‘-mno-red-zone’ part used here as well — it is a bad idea to use a red zone for kernel code if interrupts are to be implemented. It should be done with GCC as well, but read Libgcc without red zone for the extra work needed to be done.

Creating the FAT image

Main article: Bootable Disk

Next, create a FAT filesystem image.

dd if=/dev/zero of=fat.img bs=1k count=1440
mformat -i fat.img -f 1440 ::
mmd -i fat.img ::/EFI
mmd -i fat.img ::/EFI/BOOT
mcopy -i fat.img BOOTX64.EFI ::/EFI/BOOT

Running as a USB stick image

The FAT image can either be written directly to a USB stick and used in in a UEFI machine, or it can be run directly in QEMU:

qemu-system-x86_64 -L OVMF_dir/ -pflash OVMF.fd -usb -usbdevice disk::fat.img

Creating and running the HD image

The HD image is a disk image in the GPT format, with the FAT image specially identified as a ‘EFI System Partition’.

mkgpt -o hdimage.bin --image-size 4096 --part fat.img --type system
qemu-system-x86_64 -L OVMF_dir/ -pflash OVMF.fd -hda hdimage.bin

Creating and running the CD image

The ISO image is a standard ISO9660 image which contains the FAT image as a file. A special El Torito option (-e) then points EFI aware systems to this image to be loaded. The CD image can either be burned to a CD and ran in a UEFI machine, or run directly in QEMU:

mkdir iso
cp fat.img iso
xorriso -as mkisofs -R -f -e fat.img -no-emul-boot -o cdimage.iso iso
qemu-system-x86_64 -L OVMF_dir/ -pflash OVMF.fd -cdrom cdimage.iso

What to do next?

Developers may want to try using some more of the EFI boot services, e.g., to read more files from the FAT image, manage memory, set up graphical frame buffer etc. (see the UEFI Specifications page for further documentation of this).

There is also a finished app bare bone which supports both Linux and Windows (Visual Studio), see uefi-simple.

Common problems

Some UEFI hardware implementations require that the FAT image is in the FAT32 format (rather than FAT12 or FAT16). OVMF does not have this limitation, so developers will not see such a problem in QEMU. However, the minimum size of a FAT32 filesystem is around 32 MiB, so developers will need to generate a much larger image and pass the ‘-F’ option to mformat.

See also

  • UEFI
  • UEFI ISO Bare Bones
  • GNU-EFI
  • POSIX-UEFI

Страница 1 из 2


  1. abcd008

    abcd008

    New Member

    Публикаций:

    0

    Регистрация:
    8 фев 2009
    Сообщения:
    616

    как загрузить свою ос если вместо bios стоит uefi ?


  2. Pavia

    Pavia

    Well-Known Member

    Публикаций:

    0

    Регистрация:
    17 июн 2003
    Сообщения:
    2.409
    Адрес:
    Fryazino

    abcd008
    Разберись и напеши нам тоже интересно.


  3. iZzz32

    iZzz32

    Sergey Sfeli

    Публикаций:

    0

    Регистрация:
    3 сен 2006
    Сообщения:
    355

    Сюда и дальше по ссылкам?


  4. abcd008

    abcd008

    New Member

    Публикаций:

    0

    Регистрация:
    8 фев 2009
    Сообщения:
    616

    спасибо, посмотрел. немного разобрался, но не совсем.
    у меня с англ. плоховато, есть ссылки на русском?


  5. abcd008

    abcd008

    New Member

    Публикаций:

    0

    Регистрация:
    8 фев 2009
    Сообщения:
    616

    возник еще вопрос: а в uefi есть поддержка видео сервиса через int 10h?


  6. Pavia

    Pavia

    Well-Known Member

    Публикаций:

    0

    Регистрация:
    17 июн 2003
    Сообщения:
    2.409
    Адрес:
    Fryazino

    abcd008
    Нету там int. Зато есть. EFI_GRAPHICS_OUTPUT_PROTOCOL


  7. dgs

    dgs

    New Member

    Публикаций:

    0

    Регистрация:
    23 июн 2008
    Сообщения:
    434

    Какое грозное название… х)


  8. abcd008

    abcd008

    New Member

    Публикаций:

    0

    Регистрация:
    8 фев 2009
    Сообщения:
    616

    получается для uefi нато полностью переписывать программы, которые были написаны для bios функций?


  9. Pavia

    Pavia

    Well-Known Member

    Публикаций:

    0

    Регистрация:
    17 июн 2003
    Сообщения:
    2.409
    Адрес:
    Fryazino

    abcd008
    Конечно. BIOS устарел вмести с 16 битном режимом. Поэтому и придумали EFI.


  10. abcd008

    abcd008

    New Member

    Публикаций:

    0

    Регистрация:
    8 фев 2009
    Сообщения:
    616

    а новыми функциями uefi можно пользоваться прямо из ядра системы.


  11. abcd008

    abcd008

    New Member

    Публикаций:

    0

    Регистрация:
    8 фев 2009
    Сообщения:
    616

    если в uefi нет поддердки старых сервисов, значит там больше нельзя запускать dos?

    я где-то читал, что для совместимисти можно загрузиться через mbr, если есть такая совместимость состаруми системами, почему нет сервисов int’s?


  12. Pavia

    Pavia

    Well-Known Member

    Публикаций:

    0

    Регистрация:
    17 июн 2003
    Сообщения:
    2.409
    Адрес:
    Fryazino

    abcd008
    В UEFI нет, но ты можешь запустить компьютер в усторевшим режиме. Обычный режим запуска компьютера и там будут int а вот как к UEFI ты добирешься и будет он роботать то скорее всего нет.


  13. abcd008

    abcd008

    New Member

    Публикаций:

    0

    Регистрация:
    8 фев 2009
    Сообщения:
    616

    получается при загрузке есть выбор как грузиться(там получается две прошивки bios и uefi)??
    а в uefi осталасб таблица _MP_ или все через acpi


  14. Pavia

    Pavia

    Well-Known Member

    Публикаций:

    0

    Регистрация:
    17 июн 2003
    Сообщения:
    2.409
    Адрес:
    Fryazino

    Не следует воспринимать UEFI как нечто отдельное. Это просто развитие биоса. Пока от старых веще не отказываются. Но к этому надо готовить себя.
    _MP_ устарел и его заменил ACPI. Из UEFI есть доступ к ACPI.


  15. Pavia

    Pavia

    Well-Known Member

    Публикаций:

    0

    Регистрация:
    17 июн 2003
    Сообщения:
    2.409
    Адрес:
    Fryazino

    Ты статью что не читал? Там прямым текстом написанно выбираем как грузиться.


  16. abcd008

    abcd008

    New Member

    Публикаций:

    0

    Регистрация:
    8 фев 2009
    Сообщения:
    616

    статью я читал. просто я думал там выбор откуда грузить (mbr или grub), а оказалось это выбор режима bios или legacy


  17. Pavia

    Pavia

    Well-Known Member

    Публикаций:

    0

    Регистрация:
    17 июн 2003
    Сообщения:
    2.409
    Адрес:
    Fryazino

    Сегодня целый день рыл.
    Что нашел открытый проект UEFI от intel.
    https://www.tianocore.org/

    Так вот в EDK есть папка DUET( Developer’s UEFI Emulation) Который позволяет запустить EFI на любой платформе с любым биосом. EFI грузится с флешки. Дальше можем наслождаться.
    Завтро если будет время. Попробую собрать.


  18. abcd008

    abcd008

    New Member

    Публикаций:

    0

    Регистрация:
    8 фев 2009
    Сообщения:
    616

    Pavia
    прикольная вещь. я нашел сайт с уже собранной duet efi, зарегистрировался, но мне пишут, что у мена нет прав доступа для скачивания.
    — http://www.applelife.ru/laboratoriya_apple_life67/efi_na_pc_chast_tretya_teoreticheskaya/14542.html

    если у тебя собирется, скинь.


  19. Pavia

    Pavia

    Well-Known Member

    Публикаций:

    0

    Регистрация:
    17 июн 2003
    Сообщения:
    2.409
    Адрес:
    Fryazino

    Откомпилировал работает =) Правда не все так как бетта, но многое.


  20. abcd008

    abcd008

    New Member

    Публикаций:

    0

    Регистрация:
    8 фев 2009
    Сообщения:
    616

Страница 1 из 2


WASM

Введение

В этой статье будут рассмотрены базовые вещи, касающиеся программирования текстового ввода-вывода. В программе, которую на этот раз создадим сами с нуля, вначале введем с клавиатуры, а потом выведем на экран текстовую строку, коснувшись по пути некоторых неочевидных особенностей программирования под UEFI.

Вторая половина статьи будет про ускорение загрузки драйвера при запуске на отладку, поскольку две минуты, что тратятся на это сейчас, при многократном запуске в процессе отладки своего драйвера будут сильно раздражать.

Те, кто заинтересовался — добро пожаловать под кат.

Подготовка

Сразу же сделаем свой каталог для наших упражнений в корне edk2 и назовем его EducationPkg. Все проекты будем создавать внутри него. Можно создавать в корне edk2 и каждый отдельный проект, никаких препятствий для этого нет, но примерно на десятом проекте в корне разведется зоопарк из своих проектов и пакетов фреймворка edk2, что приведет, в лучшем случае, к путанице. Итак, создаем каталог C:FWedk2EducationPkg.

Использование UEFI Driver Wizard

Созание набора файлов для проекта edk2 — большая тема, заслуживающая отдельной статьи. Можно про это почитать, если хочется узнать правду прямо сейчас, в документе EDK II Module Writer’s Guide, глава 3 Module Development – здесь или погуглите. Мы же пока будем использовать для создания набора файлов интеловскую утилиту UEFI Driver Wizard, которая лежит в скачанном каталоге в C:FWUEFIDriverWizard

Запускаем UEFI Driver Wizard, и первое, что надо сделать – указать рабочую среду Workspace C:FWedk2 по команде File → OPEN WORKSPACE. Если этого не сделать вначале, то UEFI Driver Wizard заботливо проведет нас по всем этапам создания, а потом, с извинениями, скажет, что проект драйвера создать не может.

После указания Workspace выбираем File → New UEFI Driver и производим следующие действия:

1. Жмем на кнопку Browse, заходим в каталог C:FWedk2EducationPkg и создаем внутри него свой каталог MyFirstDriver, в котором и будем работать дальше. Имя драйвера примет то же название

2. Выставляем Driver Revision в 1.0. Меньше единицы выставлять не советую — в Wizard ошибка, в результате которой ревизия получается 0.0 после создания файлов проекта.

3. Все остальное оставляем без изменений, так чтобы получилось, как показано на скриншоте

Жмем кнопку Next и переходим к следующему экрану:

Ставим галочки напротив Component Name Protocol и Component Name 2 Protocol, остальное не трогаем:

и после сразу жмем Finish, не трогая другие настройки. Получаем сообщение об успешном создании проекта:


Жмем Ок и закрываем UEFI Driver Wizard, он нам больше не понадобится.

Добавление модуля в список для компиляции

Поскольку в мире ничего идеального нет, нам придется вручную добавить строчку для нашего драйвера в список модулей для компиляции. Тут надо остановиться и дать небольшую справку.

Все модули в edk2 входят в т.н. рackages – группы модулей, которые объединены между собой по какому-то общему признаку. Чтобы включить новый модуль в состав какого-либо package, нам необходимо добавить путь к исходникам модуля в файл PackageName.dsc (dsc – сокращение от Description), и этот модуль будет компилироваться в составе Package. UEFI Wizard Driver наш драйвер для компиляции добавить автоматически, увы, не может по объективным причинам (ну откуда ему знать, что за свой новый package вы создали (или с каким из существующих намерены работать?). Поэтому – прописываем ручками.

Просьба понимать, что наш EducationPkg — пока не package, а всего лишь набор проектов, и не более того.

Открываем файл C:FWedk2Nt32PkgNt32Pkg.dsc, ищем в нем строчку

# Add new modules here

и после этой строки прописываем путь к файлу MyFirstDriver.inf нашего драйвера относительно C:FWedk2. Должно получиться вот так:

# Add new modules here
EducationPkg/MyFirstDriver/MyFirstDriver.inf
##############################################################################

Очень важное лирическое отступление

Теперь вы уныло думаете, что придется снова настраивать проект в Visual Studio, и совершенно напрасно. Вспомните, мы в первой статье нигде не указали, о каком именно проекте идет речь. Поэтому править ничего не надо, компиляция после добавления в Nt32Pkg нашего inf-файла пойдет и так. Это приводит нас к очень важному следствию: мы можем добавить в уже имеющийся проект Visual Studio, например, NT32, файлы любого проекта из огромного дерева edk2 и все они будут доступны на редактирование и – это главное – постановку контрольных точек, просмотра Watch и всех остальных возможностей, которые предлагает Visual Studio. Собственно, в этом и состоит одно из наиболее интересных преимуществ данного подхода к работе в UEFI с помощью Visual Studio. И при нажатии на F5 перед началом компиляции будет произведено автосохранение измененных исходников, добавленных в проект. Мы, правда, платим за этот подход увеличенным временем компиляции, но дальше разберемся и с этой проблемой.

Давайте проделаем то, о чем только что говорили, для нашего проекта. Щелкаем в Visual Studio правой клавишей на проекте NT32 (не забыв переключить его в проект по умолчанию в Solution), выбираем Add → Existing Item, переходим в наш каталог

C:FWedk2EducationPkgMyFirstDriver

выделяем мышкой все файлы и жмем на Add, добавляя их в наш проект NT32.

Компиляция и запуск драйвера

Жмем в Visual Studio на F5 или кнопку Debugging, ждем загрузки Shell, в ней вводим:

fs0:
load MyFirstDriver.efi

И затем смотрим на сообщение об успешной загрузке нашего драйвера. Он ничего не делает, что совсем не удивительно – никакую функциональность в него мы еще не добавляли. Нам надо лишь проверить, что мы правильно добавили наш драйвер в среду edk2, и не более того.
Вот то, что мы должны видеть (адрес может быть любой):


Закрываем окно нажатим кнопки Stop Debugging, или Shift + F5 в Visual Studio.

Добавление своего кода

Открываем в Visual Studio файл MyFirstDriver.c из получившегося дерева проекта и добавляем в него наш код. Но вначале немного теории, перед тем, как перейдем к практике.

В UEFI BIOS все взаимодействие с «железом» — в нашем случае, его эмуляцией на виртуальной машине – происходит через протоколы, вот прямо так взять и записать определенный байт в определенный порт — нельзя. В очень упрощенном виде можно рассматривать протокол как имя “класса”, экземпляр которого создается для работы с устройством при его регистрации в системе. Как и обычный экземпляр класса, протокол содержит данные и функции, и вся работа с аппаратурой обеспечивается вызовом соответствующих приватных функций класса.

При работе в UEFI используются таблицы, в которых содержатся указатели на все экземпляры «классов». Этих таблиц несколько, в случае с нашим драйвером мы используем System Table, используя уже объявленный указатель gST. Иерархия этих таблиц довольно проста: есть основная таблица System Table, в которой содержатся (среди прочего) ссылки на таблицы Boot Services и Runtime Services. Впрочем, наверное, будет проще показать код:

gST = *SystemTable; 
gBS = gST->BootServices; 
gRT = gST->RuntimeServices;

По названиям переменных: gST расшифровывается как:
g — означает, что переменная глобальная
ST, как вы уже догадались, означает System Table

Итак, наша задача – послать на устройство вывода (в нашем случае – дисплей) строку I have written my first UEFI driver. Хорошо бы, конечно, при этом в unix-style использовать ту же printf и просто указать разные потоки, но увы – с использованием printf в UEFI есть очень серьезные ограничения, поэтому пока давайте оставим ее в сторонке, до лучших времен.

Вставим вывод нашей строки в функцию EntryPoint() нашего драйвера. Добавляем в области объявления переменных нашу переменную типа CHAR16 (используется двухбайтовая кодировка символов UCS-2) в функции MyFirstDriverDriverEntryPoint() в MyFirstDriver.c:

CHAR16 *MyString = L"I have written my first UEFI driverrn";

Лирическое отступление

edk2 не прощает Warning-ов, для него все едино – что Warning, что Error – он посылает на, хм, исправление в обоих случаях, так опции компилятора в edk2 настроены по дефолту. Он, таким образом, неявно говорит «Пионэрам здесь не место». Отключить эту опцию в конфигах, разумеется, можно, но не стоит здесь объяснять, как это сделать – если вы сможете ее отключить, то и убрать источники Warning-ов в коде тоже сможете, и вам это не нужно. Поэтому приводите типы явно и комментируйте неиспользуемые переменные. Иногда, в случае использования сторонних исходников, полное подавление ошибок может оказаться довольно непростой задачей.

После, в самом конце функции MyFirstDriverDriverEntryPoint() вставьте код вывода нашей текстовой переменной на консоль вывода (экран по дефолту, в нашем случае):

gST->ConOut->OutputString(gST->ConOut, MyString);

Жмем на F5, вводим fs0:, load MyFirstDriver.efi, и получаем нашу строку на экране:

Первая программа с нуля написана и работает. Можем себя поздравить.

Разберем сейчас эту нашу строчку:

gST->ConOut->OutputString(gST->ConOut, MyString);

В ней:
gST – указатель на таблицу System Table
ConOutпротокол, или, как мы его условно обозвали, «класс» вывода текста
OutputString – сама функция вывода. Первый параметр в ней, как легко догадаться – this для нашего протокола EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL.

Давайте переформулируем вышесказанное в другом виде, для лучшего понимания. Вот иерархия главной таблицы System Table, получена в окне Watch при останове на breakpoint в Visual Studio. Подсвечена используемая нами функция OutputString. Обратите также внимание на элементы BootServices и RuntimeServices в нижней части таблицы, о чем шла речь ранее:

Ввод текста с клавиатуры и вывод его на экран

Хорошо, мы вывели текст, содержащийся в строковой константе. Теперь надо чего-то ввести в строковую переменную и затем вывести эту переменную на экран, чтобы убедиться, что нигде не ошиблись в процессе.

Сделаем еще один скриншот той же gST, но уже в части ConIn. Выглядит он вот так:


В нашем следующем примере будем запоминать вводимый с клавиатуры пароль, отображая вводимые символы разноцветными звездочками в новогоднем (или наркоманском, как заметил один товарищ) стиле, а после нажатия Enter — выводить пароль в следующей строке. Это всего лишь учебный пример, замещение вводимого пароля звездочками уже реализовано в HII API, равно как и повторный ввод пароля, и проверка совпадения повторного ввода.

Дисклаймер

По фэн-шуй надо бы сделать «упрощающий» указатель *ConOut = gST->ConOut, но в целях обучения – оставим как есть, чтобы не вспоминать, к какой из таблиц обращаемся в данный момент.

Вначале добавим локальные переменные в функции MyFirstDriverDriverEntryPoint() вместо нашей ранее добавленной переменной MyString.

  UINTN					EventIndex;
  UINTN					Index = 0;
  EFI_INPUT_KEY				Keys;
  CHAR16		        	PasswordString[256];

Теперь вводим текст программы, заместив ранее добавленную нами функцию

gST->ConOut->OutputString(gST->ConOut, MyString);

на следующее:

gST->ConOut->ClearScreen(gST->ConOut); // Надеюсь, понятно
gST->ConOut->SetCursorPosition(gST->ConOut, 3, 10); // Ставим курсор в 10-ю строку, 3-ю позицию
gST->ConOut->OutputString(gST->ConOut, L"Enter password up to 255 characters lenght, then press 'Enter' to continuern"); //Выводим нашу строку с вышеуказанной позиции
	do {
                //ждем нажатия на клавишу
		gBS->WaitForEvent (1, &gST->ConIn->WaitForKey, &EventIndex);
                // считываем код нажатия на клавишу 
		gST->ConIn->ReadKeyStroke (gST->ConIn, &Keys);
                //Изменяем цвета шрифта и фона	
		gST->ConOut->SetAttribute(gST->ConOut, Index & 0x7F);  
                // формируем строку для вывода
		PasswordString[Index++] = (Keys.UnicodeChar); 
                // Заменяем текст на звездочки – пароль же, враги рыщут повсюду 
		gST->ConOut->OutputString(gST->ConOut, L"*");
	} //пока не нажали Enter или пока не ввели 255 символов, заполняем текстовый массив вводимыми буквами
           while (!(Keys.UnicodeChar == CHAR_LINEFEED || Keys.UnicodeChar == CHAR_CARRIAGE_RETURN || Index == 254)); 
// Терминация текстовой строки, занимает 1 элемент массива
PasswordString[Index++] = '';
// Возвращаем исходные цвета шрифта и фона
gST->ConOut->SetAttribute(gST->ConOut, 0x0F);
// Дальше понятно, уже проходили
gST->ConOut->OutputString(gST->ConOut, L"rnEntered password is: rn"); 
gST->ConOut->OutputString(gST->ConOut, PasswordString);
gST->ConOut->OutputString(gST->ConOut, L"rn"); 

Жмем F5 и устраиваемся в уже ставшей привычной позе на пару минут:

После приглашения Shell, как всегда, вводим fs0: и load MyFirstDriver (или load my и затем два раза жмем на Tab, ибо Shell). Получаем такую вот картинку (разумеется, текст будет тот, что вы ввели сами):

Жмем Shift+F5, чтобы закрыть отладку, после того, как налюбовались.

Дальше в статье будем знакомиться ближе со средой UEFI Shell, PCD и отладочными сообщениями — без этого двигаться дальше нельзя. Но знакомиться будем не абстрактно, а в полезном процессе ускорения и автоматизации запуска драйвера на отладку.

Создание и редактирование загрузочного скрипта UEFI Shell

Теперь, поскольку вы, вероятно, уже перекомпилировали программу не раз и не два, то морально дозрели до того, чтобы сократить это раздражающее время ожидания загрузки UEFI Shell и вбивания каждый раз одних и тех же команд для загрузки нашего драйвера. Начнем с того, что уберем весь ручной ввод текста в Shell написанием соответствующего скрипта. Писать скрипт будем прямо в Shell. С точки зрения здравого смысла, лучше открыть файл скрипта в Far Manager и отредактировать его там, но мало ли какими судьбами в будущем вас выбросит в Shell реальной машины, а не виртуалки с доступом к ее файловой системе с хоста. Поэтому один раз создадим скрипт в редакторе Shell и запишем его, чтобы получить соответствующий навык.

Файл скрипта, исполняемый при старте (подобный autoexec.bat или bashrc) для UEFI Shell, называется startup.nsh, тот самый, что Shell каждый раз предлагает нам пропустить при загрузке. Загрузитесь в UEFI Shell, нажав в Visual Studio клавишу F5, и введите fs0:, чтобы перейти в нашу файловую систему. Теперь из Shell введем команду

edit startup.nsh

и в открывшемся редакторе введем, с переводом строки по Enter:

FS0:
load MyFirstDriver.efi

Дальше жмем F2, затем Enter для записи и затем F3 для выхода из редактора обратно в Shell.

Перекомпилять все заново на этот раз не будем, поскольку мы в программе ничего не меняли. Наберем в Shell команду Exit, и в открывшемся текстовом окне a-la BIOS Setup, а в терминах UEFIMain Form, выберем пункт Continue. После этого нас снова выбросит в Shell, но в этот раз исполнится созданный нами скрипт startup.nsh и автоматом запустится наш драйвер.

Отладочные сообщения

Пока мы можем писать отладочные сообщения на экран, но когда будем работать с формами HII (Human Interface Infrastructure) – такой возможности не представится, экран будет занят формами конфигурирования аппаратуры. Что делать в этом случае?

Несколько отвлеченная тема

В самом начале после получения новой платы с завода не всегда, ох, не всегда бывает доступна дисплейная консоль вывода отладки. По вероятности возможных ошибок подключения дисплей идет сильно впереди последовательного порта, в котором напортачить можно разве что с направлением RX-TX. Поэтому существует также и вывод в последовательный порт, полностью аналогичный выводу на экран, с заменой ConOut на StdErr. Т.е. функция

gST->ConOut->OutputString(gST->ConOut, L”Test string”);

будет выводить “Test string” на дисплей, а функция

gST->StdErr->OutputString(gST->StdErr, L”Test string”);

будет выводить тестовое сообщение в последовательный порт. Попробуйте. Позднее мы будем перенаправлять вывод в виртуальной машине из последовательного порта в лог-файл, что может оказаться полезным в будущем — писать логи на флешку, вставленную в USB порт, на реальном железе.

Вывод отладочной информации в окно OVMF

Для вывода информации в окно OVMF есть макрос DEBUG”, который обычно используется следующим образом:

DEBUG((EFI_D_INFO, "Test message is: %srn", Message));

Где первый аргумент – некий аналог линуксового уровня сообщений об ошибках ERROR_LEVEL, а второй, как можно догадаться, указатель на строку, которую надо вывести в OVMF консоль.

Первый аргумент может принимать следующие значения:

#define EFI_D_INIT 0x00000001 // Initialization style messages
#define EFI_D_WARN 0x00000002 // Warnings
#define EFI_D_LOAD 0x00000004 // Load events
#define EFI_D_FS 0x00000008 // EFI File system
#define EFI_D_POOL 0x00000010 // Alloc & Free's
#define EFI_D_PAGE 0x00000020 // Alloc & Free's
#define EFI_D_INFO 0x00000040 // Informational debug messages
#define EFI_D_VARIABLE 0x00000100 // Variable
#define EFI_D_BM 0x00000400 // Boot Manager (BDS)
#define EFI_D_BLKIO 0x00001000 // BlkIo Driver
#define EFI_D_NET 0x00004000 // SNI Driver
#define EFI_D_UNDI 0x00010000 // UNDI Driver
#define EFI_D_LOADFILE 0x00020000 // UNDI Driver
#define EFI_D_EVENT 0x00080000 // Event messages
#define EFI_D_VERBOSE 0x00400000 // Detailed debug messages that may significantly impact boot performance
#define EFI_D_ERROR 0x80000000 // Error

Регулируя уровень появления этих сообщений в логе, мы можем выдержать требуемый баланс между уровнем логгирования и производительностью системы в целом.

Еще немного о макросе DEBUG

1. Его вывод имеет форматирование и работает как форматирование printf. Для отладки очень полезен формат %r, который выводит диагностическую переменную Status не в виде 32-битного числа в HEX, а в виде человекочитаемой строки типа Supported, Invalid Argument и т.п.

2. Этот макрос автоматически отключается при смене Debug на Release, поэтому не спешите его комментировать или обвешивать ifndef-ами – все уже сделано за нас.

3. Двойные скобки там действительно нужны. Попробуйте убрать и посмотреть, что получится.

Небольшой пример

Чтобы проиллюстрировать написанное, добавим в функцию MyFirstDriverDriverEntryPoint(), сразу после объявления переменных, вывод текста из нескольких сообщений в лог с разными уровнями отладки и посмотрим, какие из них выведутся, а какие будут отфильтрованы:

DEBUG((EFI_D_INFO, "Informational debug messagesrn"));
DEBUG((EFI_D_ERROR, "Error messagesrn"));
DEBUG((EFI_D_WARN, "Warning messagesrn"));

Запускаем на отладку и смотрим в окно OVMF:

Видно, что сообщения с уровнем EFI_D_INFO и EFI_D_ERROR попали в лог, а с уровнем EFI_D_WARN – не попало.

Механизм регулирования уровней отладки достаточно прост: как мы видим в списке выше, каждый уровень характеризуется одним битом в 32-битном слове. Чтобы отфильтровать ненужные нам в данный момент сообщения, мы ставим битовую маску на значение уровня, который отсекает биты, не попадающие в данную маску. В нашем случае маска была 0x80000040, поскольку уровень EFI_D_WARN со значением 0x00000002 отфильтровался и не попал в вывод, а уровень EFI_D_INFO со значением 0x00000040, и EFI_D_ERROR с его значением 0x80000000 попали в маску и соответствующие сообщения были выведены.

Сейчас не будем дальше углубляться в рассмотрение реализации механизма настройки уровня вывода отладки, рассмотрим только способы его изменения на практике. Их два, первый из них быстрый, а второй – правильный. Начнем, понятно, с быстрого. Открываем файл c:FWNt32PkgNt32Pkg.dsc и ищем в нем строку, содержащую PcdDebugPrintErrorLevel. Вот она:

gEfiMdePkgTokenSpaceGuid.PcdDebugPrintErrorLevel|0x80000040

Изменяем значение маски на 0x80000042 и запускаем на построение и отладку снова.

Поскольку мы изменили конфигурационный файл, edk2 будет снова долго пересобирать все-все-все бинарники для Nt32Pkg, но этот процесс конечен, и вот мы видим в логе закономерные все три строчки, что и требовалось доказать:

Как сделать быстро, разобрались. Теперь сделаем правильно.

Platform Configuration Database (PCD)

Проблема предыдущего подхода в том, что дерево edk2 и пакет Nt32Pkg у нас единственные, и менять системные настройки ради единственного проекта – прямой путь в преисподнюю, ибо в лучшем случае через неделю вы про это изменение забудете напрочь и будете проклинать исчадье ада под названием edk2, что месяц назад исправно создавало из оттестированных исходников под версионным контролем именно то, что надо, а сейчас выдает нечто совершенно другое. Поэтому в edk2 реализован механизм изменения системных настроек под единственный проект, чтобы локализовать изменения этих настроек только для данного проекта. Называется этот механизм PCDPlatform Configuration Database, и позволяет очень многое. Вообще, хорошим стилем в edk2 считается выносить из исходников в PCD любой параметр, который может быть изменен в будущем. Объем статьи не позволяет остановиться на описании PCD подробнее, поэтому лучше подробности про PCD посмотреть вот здесь в п.3.7.3 или вот здесь. На первое время вполне достаточно ограничиться прочтением файла C:FWedk2MdeModulePkgUniversalPCDDxePcd.inf

С точки зрения практики, конфигурирование при помощи PCD производится вот так: в том же самом файле c:FWNt32PkgNt32Pkg.dsc изменяем уровень показа сообщений в окне OVMF:

EducationPkg/MyFirstDriver/MyFirstDriver.inf {
  <PcdsFixedAtBuild>
    gEfiMdePkgTokenSpaceGuid.PcdDebugPrintErrorLevel|0x80000042
 } 

Не спешите сразу записывать файл. Вначале поправьте обратно 0x80000042 к дефолтному значению 0x80000040 в строчке, которую мы редактировали ранее:

gEfiMdePkgTokenSpaceGuid.PcdDebugPrintErrorLevel|0x80000042

И вот теперь можно записать файл и пересобрать проект. Запускаем на отладку по F5 и видим наши заветные три строчки в отладочной консоли.

Ускоряем отладку дальше

Уберем еще пару раздражающих задержек при запуске на отладку. Очевидно, первый кандидат – это ожидание тех самых 5 сек перед запуском скрипта startup.nsh. Оно, конечно, можно и на пробел нажать, но любой

уважающий себя лентяй

программист должен автоматизировать ручные операции насколько возможно.

Теперь придется противоречить тому, что было сказано ранее. Проблема состоит в том, что в эти 5 секунд задержки прописываются не через PCD, а вопреки фэн-шуй, напрямую, в исходниках Shell. Поэтому и нам, хотим мы того или нет, придется поступить аналогично: откроем файл C:FWedk2ShellPkgApplicationShellShell.c и поменяем в инициализации значение «5» на «1»

ShellInfoObject.ShellInitSettings.Delay = 1;//5;

Можно было бы и в 0 выставить, но мало ли… Забудем поменять на реальной аппаратной системе, а возможности перекомпилировать потом не будет.

Жмем F5 и радуемся 1 сек. задержки вместо 5.

Если кто-то знает, как выставить это значение задержки правильно, через PCD и без правки исходников Shell — дайте знать в личку или в комменты, я поправлю.

Еще ускоряемся

Вспоминаем про строку прогресса, а на самом деле – просто таймера ожидания выбора опции загрузки. Вот так это выглядит на основном экране:

А в окне запуска OVMF это смотрится несколько иначе:

Как говорил Винни-Пух, «Это ж-ж-ж – неспроста!» Надо найти источник и также уменьшить до 1 сек.

Запускаем поиск по всем файлам с расширением *.c строки Zzzzz, находим эту строку в исходнике C:FWMdeModulePkgUniversalBdsDxeBdsEntry.c и видим там вот такой блок кода:

  DEBUG ((EFI_D_INFO, "[Bds]BdsWait ...Zzzzzzzzzzzz...n"));
  TimeoutRemain = PcdGet16 (PcdPlatformBootTimeOut);
  while (TimeoutRemain != 0) {
    DEBUG ((EFI_D_INFO, "[Bds]BdsWait(%d)..Zzzz...n", (UINTN) TimeoutRemain));
    PlatformBootManagerWaitCallback (TimeoutRemain);

Соответственно, понятно, что переменная TimeoutRemain читается из конфигурационной базы PCD, в параметре PcdPlatformBootTimeOut. Ок, открываем наш конфигурационный файл c:FWNt32PkgNt32Pkg.dsc, ищем там строку с PcdPlatformBootTimeOut:

  gEfiMdePkgTokenSpaceGuid.PcdPlatformBootTimeOut|L"Timeout"|gEfiGlobalVariableGuid|0x0|10

Здесь уже вариант, как ранее, с конфигурацией PcdDebugPrintErrorLevel исключительно для нашего драйвера, не получится – в данном случае задержка будет выполняться задолго до того, как наш модуль MyFirstDriver будет загружен стартовым скриптом startup.nsh, поэтому придется менять глобально, хотим мы этого или нет. В данном случае — хотим, потому что эта задержка в процессе разработки драйверов обычно ни к чему. Меняем 10 на 1 в нашем конфигурационном файле, жмем F5 и радуемся быстрой загрузке. На моей машине это занимает теперь 23 секунды.

Еще тюнинг, на этот раз — интерфейса

Убираем второй дисплей, который нам незачем пока что, а раздражать уже начал. Правим строки в нашем любимом конфигурационном файле c:FWNt32PkgNt32Pkg.dsc, открыв его и убрав !My EDK II 2 и !UGA Window 2 в двух строчках, чтобы получилось:

gEfiNt32PkgTokenSpaceGuid.PcdWinNtGop|L"UGA Window 1"|VOID|52

и

gEfiNt32PkgTokenSpaceGuid.PcdWinNtUga|L"UGA Window 1"|VOID|52

Можно, разумеется, и саму надпись в заголовке окна UGA Window 1 поменять на что-то духовно более близкое вам лично, но это уже на ваше усмотрение.

На будущее

Есть еще один большой резерв сокращения времени компиляции и запуска проекта (в разы) – компилировать не весь edk2, а только наш модуль. Но об этом, как говорится – в следующей серии. Можете пока попробовать сделать это сами, решение элементарное, как убедитесь чуть позже.

Вам, возможно, сейчас кажется излишеством — уделение такого большого внимания сокращению сроков компиляции и запуска, но поверьте — когда начнете писать свои программы, разница между целевыми 10 сек до запуска, к которым мы придем, и 2 мин, которые были вначале — огромна. Потраченное сейчас время очень быстро окупится — третья статья не за горами, а в ней отлаживаться придется много.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Выделение в статье ключевых слов жирным шрифтом…


78.33%
Полезно, улучшает восприятие текста статьи
47


5%
Не нужно и только отвлекает внимание
3

Проголосовали 60 пользователей.

Воздержались 18 пользователей.

попытка написать загрузчик

 linux, loader


1

1

здрасьте здрасьте люди добрые

хочу попытаться написать что-то вроде загрузчика для Linux.

вызыванно это влиянием нашего форума. тяжело не пытаться сделать что-то самому, когда все кругом этим занимаются.

скажите с чего начать? писать планирую в Nano.

возникают вопросы, каким должно быть расширение файла? 

и если несложно то скажите на каком языке лучше это делать?
на Assembler или лучше перевести сразу в машинные инструкции?

  • Ссылка

Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.

Предупреждение

Отключите secure boot! На данный момент эта технология не поддерживается. Чтобы настроить процесс загрузки с помощью GRUB для UEFI, необходимо отключить её в интерфейсе конфигурации прошивки. Прочтите документацию, предоставленную производителем вашей системы, чтобы узнать, как это сделать.

Убедитесь, что вы не пропустили раздел по настройке ядра, для поддержки EFI.

Поиск, или создание системного раздела EFI⚓︎

В системе на основе EFI загрузчики устанавливаются в специальный раздел FAT32, называемый системным разделом EFI (ESP). Если ваша система поддерживает EFI и предустановлен дистрибутив Linux и (или) Windows, скорее всего, ESP уже создан. Посмотрите все разделы на вашем жёстком диске (замените sda на нужное устройство):

Столбец ESP type должен быть EFI System.

Например:

Устр-во    начало     Конец   Секторы Размер Тип
/dev/sda1    4096    618495    614400   300M EFI
/dev/sda2  618496 268430084 267811589 127,7G Файловая система Linux

Если система или жёсткий диск новые, или если вы впервые устанавливаете ОС, загружаемую через UEFI, ESP может не существовать. В этом случае создайте новый раздел, создайте на нем файловую систему vfat и установите тип раздела EFI system.

Bug

Некоторые (старые) реализации UEFI могут требовать, чтобы ESP был первым разделом на диске.

Создайте точку монтирования для ESP и смонтируйте ее (замените sda1 на соответствующий ESP):

mkdir -pv /boot/efi &&
mount -v -t vfat /dev/sda1 /boot/efi

Добавьте запись для ESP в /etc/fstab, чтобы он автоматически монтировался во время загрузки системы:

cat >> /etc/fstab << EOF
/dev/sda1 /boot/efi vfat defaults 0 1
EOF

Монтирование EFI Variable File System⚓︎

Для установки GRUB на UEFI необходимо смонтировать файловую систему EFI Variable, efivarfs. Если она еще не была смонтирована ранее, выполните команду:

mountpoint /sys/firmware/efi/efivars || mount -v -t efivarfs efivarfs /sys/firmware/efi/efivars

Добавьте запись для efivarfs в /etc/fstab, чтобы она автоматически монтировалась во время загрузки системы:

cat >> /etc/fstab << EOF
efivarfs /sys/firmware/efi/efivars efivarfs defaults 0 0
EOF

Обратите внимание

Если система не загружается с UEFI, каталог /sys/firmware/efi будет отсутствовать. В этом случае вы должны загрузить систему в режиме UEFI с аварийным загрузочным диском.

Настройка⚓︎

В системах на основе UEFI GRUB работает устанавливая приложение EFI (особый вид исполняемого файла) в /boot/efi/EFI/[id sizes/grubx64.efi, где /boot/efi — точка монтирования ESP, а [id] заменяется идентификатором, указанным в командной строке grub-install. GRUB создаст запись в переменных EFI, содержащую путь EFI/[id]/grubx64.efi, чтобы прошивка EFI могла найти grubx64.efi и загрузить его.

grubx64.efi очень легкий (136 Кб), поэтому он не будет занимать много места в ESP. Типичный размер ESP составляет 100 Мб (для диспетчера загрузки Windows, который использует около 50 Мб в ESP). Как только grubx64.efi загружен прошивкой, он загрузит модули GRUB в загрузочный раздел. Расположение по умолчанию — /boot/grub.

Установите файлы GRUB в /boot/efi/EFI/LFS/grubx64.efi и /boot/grub. Затем настройте загрузочную запись в переменных EFI:

grub-install --bootloader-id=LIN --recheck

Если установка прошла успешно, вывод должен быть:

Installing for x86_64-efi platform.
Installation finished. No error reported.

Запустите efibootmgr, чтобы ещё раз проверить конфигурацию загрузки EFI.

Пример вывода:

BootCurrent: 0000
Timeout: 1 seconds
BootOrder: 0005,0000,0002,0001,0003,0004
Boot0000* ARCH
Boot0001* UEFI:CD/DVD Drive
Boot0002* Windows Boot Manager
Boot0003* UEFI:Removable Device
Boot0004* UEFI:Network Device
Boot0005* LIN

Обратите внимание, что 0005 является первым в BootOrder, а Boot0005 — это LIN. Это означает, что при следующей загрузке системы будет использоваться версия GRUB, установленная в LIN.

Создание файла конфигурации GRUB⚓︎

Создайте /boot/grub/grub.cfg для настройки меню загрузки GRUB:

cat > /boot/grub/grub.cfg << EOF
# Begin /boot/grub/grub.cfg
set default=0
set timeout=5

insmod part_gpt
insmod ext2
set root=(hd0,2)

if loadfont /boot/grub/fonts/unicode.pf2; then
  set gfxmode=auto
  insmod all_video
  terminal_output gfxterm
fi

menuentry "GNU/Linux, Linux 5.10.17-lfs-10.1"  {
  linux   /boot/vmlinuz root=/dev/sda2 ro
}

menuentry "Firmware Setup" {
  fwsetup
}
EOF

(hd0,2), sda2 следует заменить в соответствии с вашей конфигурацией.

Обратите внимание

Для GRUB файлы используются относительно раздела. Если вы использовали отдельный раздел /boot, удалите /boot из указанных выше путей (к ядру и к unicode.pf2). Вам также нужно будет изменить строку корневого раздела, чтобы она указывала на загрузочный раздел.

Загрузка вместе с Windows⚓︎

Добавьте запись в файл конфигурации grub.cfg:

cat >> /boot/grub/grub.cfg << EOF
# Begin Windows addition

menuentry "Windows 10" {
  insmod fat
  insmod chain
  set root=(hd0,1)
  chainloader /EFI/Microsoft/Boot/bootmgfw.efi
}
EOF

(hd0,1) следует заменить назначенным GRUB именем для ESP. Директива chainloader может использоваться, чтобы указать GRUB запустить другой исполняемый файл EFI, в данном случае диспетчер загрузки Windows. вы можете поместить больше используемых инструментов в исполняемом формате EFI (например, оболочку EFI) в ESP и создать для них записи GRUB.

Понравилась статья? Поделить с друзьями:
  • Как написать trash talk
  • Как написать trap beat
  • Как написать telegram bot python
  • Как написать tab
  • Как написать synthwave