# UEFI

# Основная информация

(U)EFI или (Унифицированный) Расширяемый Интерфейс микропрограммного обеспечения - это спецификация для платформ x86, x86-64, ARM и Itanium, которая определяет программный интерфейс между операционной системой и микропрограммным обеспечением платформы/BIOS. Оригинальный EFI был разработан в середине 1990-х годов компанией Intel для разработки встроенного ПО/BIOS для платформ Itanium. В 2005 году Intel передала спецификацию новой рабочей группе под названием Unified EFI Forum, состоящей из таких компаний, как AMD, Microsoft, Apple и сама Intel. Все современные ПК поставляются с прошивкой UEFI, UEFI широко поддерживается как коммерческими, так и операционными системами с открытым исходным кодом. Обратная совместимость предусмотрена для устаревших операционных систем.

# Основы UEFI

# Загрузка образов UEFI

Если вы используете VirtualBox для виртуализации, то UEFI уже включен, нет необходимости загружать образ вручную. Вам просто нужно включить его в настройках виртуальной машины, нажав флажок "Настройки" / "Системы" / "Включить EFI (только специальные операционные системы)".

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

В Linux вы также можете установить их с помощью диспетчера пакетов вашего дистрибутива, например:

# Debian / Ubuntu

apt-get install ovmf

# RedHat / CentOS

yum install ovmf

# macOS

Используйте репозиторий OVMF-blobs.

# Windows

Используйте репозиторий OVMF-blobs или загрузите RPM-версию, затем с помощью 7-Zip распакуйте файл OVMF.fd из загруженного архива.

# UEFI против BIOS

Распространенным заблуждением является то, что UEFI является заменой BIOS. На самом деле, как устаревшие материнские платы, так и материнские платы на основе UEFI поставляются с ПЗУ BIOS, которые содержат встроенное ПО, которое выполняет начальную настройку системы при включении питания, прежде чем загружать какой-либо сторонний код в память и переходить к нему. Различия между устаревшей прошивкой BIOS и прошивкой UEFI BIOS заключаются в том, где они находят этот код, как они готовят систему перед переходом к ней и какие удобные функции они предоставляют для вызова кода во время работы.

# Инициализация платформы

BIOS выполняет всю инициализацию платформы (конфигурация контроллера памяти, конфигурация шины PCI и BAR-mapping, инициализация видеокарты и т.д.), но затем переходит в обратно совместимую среду Real Mode. Загрузчик должен включить A20-gate, настроить GDT и IDT, переключиться в Protected Mode, а для процессоров x86-64 настроить подкачку и переключиться в Long Mode.

Прошивка UEFI выполняет те же шаги, но также подготавливает среду Protected Mode с плоской сегментацией, а для процессоров x86-64-среду Long mode с отображением идентификаторов подкачки. A20-gate также включён.

Кроме того, процедура инициализации платформы прошивки UEFI стандартизирована. Это позволяет распространять прошивку UEFI независимо от поставщика платы.

# Механизм загрузки

BIOS загружает 512-байтовый двоичный объект из загрузочного устройства типа MBR(Master Boot Record) в память по физическому адресу 7C00 и переходит к нему. Загрузчик не может вернуться обратно в BIOS. Прошивка UEFI загружает приложение UEFI произвольного размера (исполняемый файл PE) из раздела FAT на загрузочном устройстве с разделом GPT на некоторый адрес, выбранный во время выполнения. Затем он вызывает основную точку входа этого приложения. Приложение может вернуть управление встроенному ПО, которое продолжит поиск другого загрузочного устройства или вызовет меню диагностики.

# Обнаружение системы

BIOS сканирует память на наличие таких структур, как таблицы EBDA, SMBIOS и ACPI. Он использует PIO для связи с корневым контроллером PCI и сканирования шины PCI. Возможно, что в памяти могут присутствовать избыточные таблицы (например, таблица MP в SMBIOS содержит информацию, которая также присутствует в DSDT ACPI), и загрузчик может выбрать, какие из них использовать.

Когда UEFI вызывает функцию точки входа UEFI-приложения, она передает структуру "Системной таблицы", которая содержит указатели на все таблицы ACPI системы, карту памяти и другую информацию, относящуюся к ОС. Устаревшие таблицы (например, SMBIOS) могут отсутствовать в памяти.

# Удобные функции

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

UEFI устанавливает в памяти множество вызываемых функций, которые группируются в наборы, называемые "протоколами", и которые можно обнаружить через системную таблицу. Поведение каждой функции в каждом протоколе определяется спецификацией. Приложения UEFI могут определять свои собственные протоколы и сохранять их в памяти для использования другими UEFI-приложениями. Функции вызываются с помощью стандартизированного, современного стандарта системных вызовов, поддерживаемого многими компиляторами языка C.

# Среда разработки

Устаревшие загрузчики могут быть разработаны в любой среде, которая может генерировать двоичные образы: NASM, GCC и т.д. Приложения UEFI могут быть разработаны на любом языке, который может быть скомпилирован и связан в исполняемый файл PE и поддерживает соответствующие вызовы, используемые для доступа к функциям, установленным в памяти прошивкой UEFI. На практике это означает одну из двух сред разработки: Intel TianoCore EDK2, GNU-EFI или POSIX-UEFI.

TianoCore - это большая, сложная среда с собственной системой сборки. Его можно настроить для использования вместе с GCC, MinGW, Microsoft Visual C++ и т.д. Его можно использовать не только для компиляции UEFI-приложений, но и для компиляции прошивки UEFI, которая будет перенесена в ПЗУ BIOS.

GNU-EFI - это набор библиотек и заголовков для компиляции приложений UEFI с собственным GCC системы (не работает с LLVM CLang). Он не может быть использован для компиляции прошивки UEFI. Поскольку это всего лишь пара библиотек, с которыми можно связать UEFI-приложение, его гораздо проще использовать, чем TianoCore.

POSIX-UEFI очень похож на GNU-EFI, но он распространяется в основном как исходный код, а не как двоичная библиотека, имеет имена, подобные ANSI C, и работает с GCC, а также с LLVM CLang. Он поставляется с файлом Makefile.

# Эмуляция

Bochs поставляется с BIOS с открытым исходным кодом по умолчанию. Кроме того, SeaBIOS, популярный BIOS, который был портирован как на эмулированные машины Bochs, так и на машины с эмуляцией QEMU. Оба этих BIOSs реализуют большинство функций BIOS, которые можно было бы ожидать. Тем не менее, они довольно значительно отличаются по эксплуатации от коммерческих BIOS на реальных машинах.

OVMF, популярная прошивка UEFI с открытым исходным кодом, была перенесена на эмулируемую машину QEMU (но не Bochs). Поскольку он реализует спецификацию UEFI, он ведет себя очень похоже на коммерческую прошивку UEFI на реальных машинах. (Сам OVMF построен с помощью TianoCore, но доступны готовые образы.)

# Загрузчик BIOS или приложение для UEFI?

Если вы ориентируетесь на устаревшие системы, для которых UEFI недоступен или ненадежен, вам следует разработать загрузчик для BIOS. Это требует глубокого знания 16-битной адресации и функций обратной совместимости процессора x86 или x86-64. Если вы ориентируетесь на современные системы, вам следует разработать UEFI-приложение. Многие прошивки UEFI могут быть сконфигурированы для эмуляции BIOS, но среди этих эмулируемых сред существует еще больше различий, чем среди реальных BIOS.

# UEFI 0-3 класса и CSM

ПК классифицируются как класс UEFI 0, 1, 2 или 3. Машина класса 0-это устаревшая система с BIOS, т.е. Вообще не система UEFI.

Машина класса 1 - это система с UEFI, которая работает исключительно в режиме модуля поддержки совместимости (CSM). CSM - это спецификация того, как прошивка UEFI может эмулировать устаревший BIOS. Прошивка UEFI в режиме CSM загружает Legacy-загрузчики. Система UEFI класса 1 может вообще не декларировать поддержку UEFI, поскольку она не доступна загрузчику. Это только UEFI "внутри" BIOS.

Машина класса 2 - это система UEFI, которая может запускать UEFI-приложения, но также включает в себя возможность запуска в режиме CSM. Большинство современных ПК - это машины класса UEFI 2. Иногда выбор для запуска UEFI-приложений против CSM - это тот или иной параметр в конфигурации BIOS, и в других случаях BIOS решит, какой из них использовать, после выбора загрузочного устройства и проверки того, у него Legacy-загрузчик или UEFI-приложение.

Машина класса 3 - это система UEFI, которая не поддерживает CSM. Машины класса 3 UEFI запускают только UEFI-приложения и не реализуют CSM для обратной совместимости с Legacy-загрузчиками.

# Безопасная загрузка (Secure Boot)

Безопасная загрузка - это схема цифровой подписи для приложений UEFI, состоящая из четырех компонентов:

  • PK: Ключ платформы
  • KEK: Ключ для обмена ключей
  • db: Белый список
  • dbx: Чёрный список

Прошивка UEFI, поддерживающая безопасную загрузку, всегда находится в одном из трех состояний:

  • Setup mode, Secure Boot off
  • User mode, Secure Boot off
  • User mode, Secure Boot on

В режиме настройки любое приложение UEFI может изменять или удалять PK, добавлять/удалять ключи из KEK, а также добавлять/удалять записи белого списка или черного списка из db или dbx.

В пользовательском режиме, независимо от того, включена или выключена Безопасная загрузка:

  • PK может быть изменен или удален только приложением UEFI, у которого уже есть текущий PK.
  • Ключи могут быть добавлены/удалены из KEK только приложением UEFI, имеющим PK.
  • Записи белого списка и черного списка могут быть добавлены/удалены из db и dbx только приложением UEFI, у которого есть любой из ключей в KEK.

Наконец, в пользовательском режиме с включенной безопасной загрузкой приложения UEFI должны соответствовать одному из следующих четырех требований для запуска:

  • Подписано, с подписью в db, а не в dbx
  • Подписано ключом в db, а не в dbx
  • Подписано ключом в КЕК
  • Не подписано, но хэш приложения находится в db, а не в dbx

Обратите внимание, что приложения UEFI не подписываются PK, если только PK также не находится в KEK.

Не все прошивки UEFI поддерживают безопасную загрузку, хотя это является обязательным требованием для Windows 8. Некоторые прошивки UEFI поддерживают безопасную загрузку, и нет возможности отключить их, что создает проблему для независимых разработчиков, которые не имеют доступа к PK или любому из ключей в KEK и, следовательно, не могут установить свой собственный ключ или подпись приложения или хэш в базу данных белого списка. Независимые разработчики должны разрабатывать используя системы, которые либо не поддерживают безопасную загрузку, либо имеют возможность отключить безопасную загрузку.

# Как использовать UEFI

Традиционные операционные системы, такие как Windows и Linux, имеют существующую программную архитектуру и большую базу кода для выполнения конфигурации системы и обнаружения устройств. С их сложными уровнями абстракции они не получают прямой выгоды от UEFI. В результате их загрузчики UEFI мало что делают, кроме подготовки среды для их запуска.

Независимый разработчик может найти больше пользы в использовании UEFI для написания полнофункциональных приложений UEFI, а не в том, чтобы рассматривать UEFI как временную среду запуска, которую можно выбросить во время процесса загрузки. В отличие от устаревших загрузчиков, которые обычно взаимодействуют с BIOS только для запуска ОС, приложение UEFI может реализовать сложное поведение с помощью UEFI. Другими словами, независимый разработчик не должен спешить покидать "UEFI-land".

Хорошей отправной точкой является написание приложения UEFI, которое использует системную таблицу для извлечения карты памяти и использует протокол "File" для чтения файлов с дисков в формате FAT. Следующим шагом может быть использование системной таблицы для поиска таблиц ACPI.

# Разработка с POSIX-UEFI

Простой способ компиляции приложений EFI в Linux (или любой другой системе, совместимой с POSIX) - это POSIX-UEFI. Он не только предоставляет хорошо известный libc-подобный API для вашего приложения EFI, но и генерирует Makefile, который поможет обнаружить и настроить набор инструментов для вас. Работает как с GNU gcc, так и с LLVM CLang.

Он имеет POSIX-измененные типы (например, uintn_t вместо UINTN), и ему не нужны стандартные заголовки EFI. Но если вы установите их из EDK2 или GNU-EFI, вы также сможете безопасно включить их, конфликтов имен не будет. Тем не менее, эти интерфейсы правильно определены, и все поля имеют точно такое же имя, как и в EDK2, так что это большое преимущество перед GNU-EFI.

ST->ConOut->OutputString(ST->ConOut, L"Hi!\r\n");

Типичный "Hello World" на UEFI выглядит примерно так:

#include <uefi.h>
 
int main(int argc, char **argv) {
  printf("Hello, world!\n");
  return 0;
}

А Makefile выглядит вот так:

TARGET = main.efi
include uefi/Makefile

Теперь просто запусти сборку командой make и на выходе получишь файл main.efi

# Разработка с GNU-EFI

GNU-EFI можно использовать для разработки как 32-разрядных, так и 64-разрядных приложений UEFI. В этом разделе будут рассмотрены только 64-разрядные приложения UEFI и предполагается, что сама среда разработки работает в системе x86_64, поэтому кросс-компилятор не требуется.

GNU-EFI включает в себя следующие вещи:

  • crt0-efi-x86_64.o: CRT0 (код инициализации среды выполнения C), обеспечивающий точку входа, которую микропрограмма UEFI вызовет при запуске приложения, которое, в свою очередь, вызовет функцию "efi_main", записанную разработчиком.
  • libgnuefi.a: Библиотека, содержащая одну функцию (_relocate), которая используется CRT0.
  • elf_x86_64_efi.lds: Скрипт компоновщика, используемый для связывания двоичных файлов ELF в приложения UEFI.
  • efi.h и другие заголовки: Удобные заголовки, которые предоставляют структуры, типы и константы, улучшают читаемость при доступе к системной таблице и другим ресурсам UEFI.
  • libefi.a: Библиотека, содержащая удобные функции, такие как вычисление CRC, вычисление длины строки и простая печать текста.
  • efilib.h: Заголовок для libefi.a.

Как минимум, 64-разрядное приложение UEFI должно будет связываться с crt0-efi-x86_64.o и libgnuefi.a с помощью скрипта компоновщика elf_x86_64_efi.lds. Скорее всего, вы захотите также использовать предоставленные заголовки и библиотеки удобства, и в этом разделе предполагается, что и в дальнейшем.

Типичный "Hello World" на UEFI выглядит примерно так:

#include <efi.h>
#include <efilib.h>
 
EFI_STATUS 
EFIAPI 
efi_main (EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable) {
  InitializeLib(ImageHandle, SystemTable);
  Print(L"Hello, world!\n");
  return EFI_SUCCESS;
}

Несколько заметок:

  • efi.h включен, поэтому мы можем использовать такие типы, как EFI_STATUS, EFI_HANDLE и EFI_SYSTEM_TABLE.
  • При создании 32-разрядного приложения UEFI EFIAPI пуст; GCC скомпилирует функцию "efi_main", используя стандартные вызовы C. При создании 64-разрядного приложения UEFI EFIAPI расширяется до "attribute((ms_abi))", и GCC скомпилирует функцию "efi_main", используя стандарт о вызовах Microsoft x64, как указано в UEFI. Только функции, которые будут вызываться непосредственно из UEFI (включая main, но также и обратные вызовы), должны использовать стандарт о вызовах UEFI.
  • "InitializeLib" и "Print" - это удобные функции, предоставляемые libefi.a с прототипами в efilib.h. "InitializeLib" позволяет libefi.a хранить ссылку на ImageHandle и SystemTable, предоставляемые BIOS. "Print" использует эти сохраненные ссылки для вывода строки, обращаясь к функциям, предоставляемым UEFI в памяти. (Позже мы увидим, как найти и вызвать функции, предоставляемые UEFI, вручную.)

Программа скомпилированна и слинкована как показано ниже:

gcc main.c                             \
      -c                                 \
      -fno-stack-protector               \
      -fpic                              \
      -fshort-wchar                      \
      -mno-red-zone                      \
      -I /path/to/gnu-efi/headers        \
      -I /path/to/gnu-efi/headers/x86_64 \
      -DEFI_FUNCTION_WRAPPER             \
      -o main.o

ld main.o                         \
     /path/to/crt0-efi-x86_64.o     \
     -nostdlib                      \
     -znocombreloc                  \
     -T /path/to/elf_x86_64_efi.lds \
     -shared                        \
     -Bsymbolic                     \
     -L /path/to/libs               \
     -l:libgnuefi.a                 \
     -l:libefi.a                    \
     -o main.so

objcopy -j .text                \
          -j .sdata               \
          -j .data                \
          -j .dynamic             \
          -j .dynsym              \
          -j .rel                 \
          -j .rela                \
          -j .reloc               \
          --target=efi-app-x86_64 \
          main.so                 \
          main.efi

В результате вы получите файл main.efi, которые будет весить 44 КБ.

# Эмуляция с QEMU и OVMF

Любой последней версии QEMU с последней версией OVMF будет достаточно для запуска приложения UEFI. Исполняемые файлы QEMU доступны для многих платформ, а образ OVMF (OVMF.fd) можно найти на веб-сайте TianoCore. QEMU (без загрузочного диска) можно вызвать, как показано ниже. Чтобы предотвратить попытку загрузки по PXE (сети) в последних версиях QEMU при отсутствии загрузочного диска, используйте -net none.

Рекомендуется использовать OVMF (для QEMU 1.6 или новее) с параметром pflash. В приведенных ниже инструкциях предполагается, что у вас есть образ OVMF, разделённый на отдельные разделы CODE и VARS.

qemu-system-x86_64 -cpu qemu64 \
  -drive if=pflash,format=raw,unit=0,file=path_to_OVMF_CODE.fd,readonly=on \
  -drive if=pflash,format=raw,unit=1,file=path_to_OVMF_VARS.fd \
  -net none

Если вы предпочитаете работать через терминал или через SSH/telnet, вы можете запустить QEMU без графической поддержки, используя флаг -nographic.

Если OVMF не найдет загрузочный диск с правильно названным приложением UEFI (подробнее об этом позже), он попадет в оболочку UEFI.

Оболочка UEFI

Вы можете просмотреть список доступных команд с помощью команды help.

# Создание образа диска

Чтобы запустить приложение UEFI, вам нужно будет создать образ диска и представить его в QEMU. Прошивка UEFI ожидает, что приложения UEFI будут храниться в файловой системе FAT12, FAT16 или FAT32 (называемой системным разделом EFI) на диске с разделением GPT. Многие прошивки поддерживают только FAT32, так что это то, что вы захотите использовать. В зависимости от вашей платформы существует несколько различных способов создания образа диска, содержащего ваше приложение UEFI, но все они начинаются с создания обнуленного файла образа диска. Минимальный размер раздела FAT32 составляет 33 548 800 байт, плюс вам понадобится место для первичной и вторичной таблиц GPT, а также некоторое свободное пространство, чтобы раздел можно было правильно выровнять. В этих примерах мы создадим образ диска размером 48 000 000 байт (93750 512-байтовых секторов или 48 МБ).

dd if=/dev/zero of=/path/to/uefi.img bs=512 count=93750

# Приложение uefi-run

Приложение uefi-run полезно для быстрого тестирования. Оно создает временный образ FAT, содержащий ваше приложение EFI, и запускает qemu.

uefi-run -b /path/to/OVMF.fd -q /path/to/qemu app.efi -- <дополнительные аргументы для QEMU>

uefi-run в настоящее время не собран для какого-либо дистрибутива. Вы можете установить его с помощью cargo (менеджер пакетов Rust) ("cargo install uefi-run").

# Linux, необходим root-доступ

Этот подход требует root-доступ и использует gdisk, losetup и mkdosfs.

Во-первых, используйте gdisk для создания таблицы разделов GPT с одним системным разделом EFI.

gdisk /path/to/uefi.img
GPT fdisk (gdisk) version 0.8.10
 
Partition table scan:
  MBR: not present
  BSD: not present
  APM: not present
  GPT: not present
 
Creating new GPT entries.
 
Command (? for help): o
This option deletes all partitions and creates a new protective MBR.
Proceed? (Y/N): y
 
Command (? for help): n
Partition number (1-128, default 1): 1
First sector (34-93716, default = 2048) or {+-}size{KMGTP}: 2048
Last sector (2048-93716, default = 93716) or {+-}size{KMGTP}: 93716
Current type is 'Linux filesystem'
Hex code or GUID (L to show codes, Enter = 8300): ef00
Changed type of partition to 'EFI System'
 
Command (? for help): w
 
Final checks complete. About to write GPT data. THIS WILL OVERWRITE EXISTING
PARTITIONS!!
 
Do you want to proceed? (Y/N): y
OK; writing new GUID partition table (GPT) to uefi.img.
Warning: The kernel is still using the old partition table.
The new table will be used at the next reboot.
The operation has completed successfully.

Теперь у вас есть образ диска с таблицей разделов GUID и неформатированный раздел EFI, начиная с сектора 2048. Если вы не отклонились от команд, показанных выше, образ диска будет использовать 512-байтовые секторы, поэтому раздел EFI начинается с байта 1 048 576 и имеет длину 46 934 528 байт.

Используйте losetup для представления раздела в Linux.

losetup --offset 1048576 --sizelimit 46934528 /dev/loop0 /path/to/uefi.img

(Если /dev/loop0 уже используется, вам нужно будет выбрать другое loopback-устройство.)

Отформатируйте раздел в FAT32 с помощью mkdosfs.

mkdosfs -F 32 /dev/loop0

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

mount /dev/loop0 /mnt

Скопируйте все приложения UEFI, которые вы хотите протестировать, в файловую систему.

cp /path/to/main.efi /mnt/

Наконец, размонтируйте раздел и освободите loopback-устройство.

umount /mnt
losetup -d /dev/loop0

uefi.img теперь представляет собой образ диска, содержащий первичные и вторичные таблицы GPT, содержащие один раздел типа EFI, содержащий файловую систему FAT32, содержащую одно или несколько приложений UEFI.

# Linux, без root-доступа

Этот подход использует parted, mformat, mcopy и может выполняться с правами пользователя.

Во-первых, используйте parted для создания первичных и вторичных заголовков GPT, а также одного раздела EFI, охватывающего тот же диапазон, что и описанный выше подход.

parted /path/to/uefi.img -s -a minimal mklabel gpt
parted /path/to/uefi.img -s -a minimal mkpart EFI FAT16 2048s 93716s
parted /path/to/uefi.img -s -a minimal toggle 1 boot

Теперь создайте новый временный файл образа, который будет содержать данные раздела EFI, и используйте mformat для форматирования его с помощью FAT16.

dd if=/dev/zero of=/tmp/part.img bs=512 count=91669
mformat -i /tmp/part.img -h 32 -t 32 -n 64 -c 1

Используйте mcopy для копирования любых приложений UEFI, которые вы хотите протестировать, в файловую систему.

mcopy -i /tmp/part.img /path/to/main.efi ::

Наконец, запишите образ раздела в образ основного диска.

dd if=/tmp/part.img of=/path/to/uefi.img bs=512 count=91669 seek=2048 conv=notrunc

uefi.img теперь представляет собой образ диска, содержащий первичные и вторичные таблицы GPT, содержащие один раздел типа EFI, содержащий файловую систему FAT16, содержащую одно или несколько приложений UEFI.

# FreeBSD, требуется root-доступ

Этот подход требует привилегий root и использует mdconfig, gpart, newfs_msdos и mount_msdosfs.

Сначала создайте узел устройства, который представляет обнуленный образ диска в виде блочного устройства. Это позволит нам работать над ним, используя стандартные инструменты разделения и форматирования.

$ mdconfig -f /path/to/uefi.img
md0

В этом примере новым блочным устройством является md0. Теперь создайте пустые первичные и вторичные таблицы GPT на устройстве.

$ gpart create -s GPT md0
md0 created

Теперь мы можем добавить раздел на диск. Мы укажем раздел "EFI", что просто означает, что GPT установит GUID этого раздела для специального типа "EFI". Не все BIOS требуют этого, и раздел по-прежнему можно будет монтировать и просматривать в обычном режиме в Linux, FreeBSD и Windows.

$ gpart add -t efi md0
md0p1 added

Затем создайте файловую систему FAT16 на новом разделе. Вы можете указать различные параметры для файловой системы, если хотите, но это не обязательно. В идеале вы бы создали раздел FAT32 для лучшей совместимости прошивки, но FreeBSD, похоже, создает разделы FAT32, которые OVMF не может прочитать.

$ newfs_msdos -F 16 md0p1
newfs_msdos: trim 2 sectors to adjust to a multiple of 9
/dev/md2p1: 93552 sectors in 11694 FAT16 clusters (4096 bytes/cluster)
BytesPerSec=512 SecPerClust=8 ResSectors=1 FATs=2 RootDirEnts=512 Media=0xf0 FATsecs=46 SecPerTrack=9 Heads=16 HiddenSecs=0 HugeSectors=93681

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

mount_msdosfs /dev/md0p1 /mnt

Скопируйте все приложения UEFI, которые вы хотите протестировать, в файловую систему.

cp /path/to/main.efi /mnt/

Наконец, размонтируйте раздел и освободите устройство.

$ umount /mnt
$ mdconfig -d -u md0

uefi.img теперь представляет собой образ диска, содержащий первичные и вторичные таблицы GPT, содержащие один раздел типа EFI, содержащий файловую систему FAT16, содержащую одно или несколько приложений UEFI.

# macOS, не требуется root-доступ

В Mac OS есть один инструмент (hdiutil), который одновременно создает образ диска и копирует файлы.

Допустим, вы создаете UEFI для x86_64. По определению имя файла должно быть BOOTX64.EFI и этот файл должны находиться в папке /EFI/BOOT.

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

mkdir -p diskImage/EFI/BOOT

Во-вторых, давайте скопируем приложение в нужную директорию:

cp bootx64.efi diskImage/EFI/BOOT/BOOTX64.EFI

Наконец, давайте создадим образ диска, разделенный GPT, отформатированный с помощью fat32 (-fs fat32), при необходимости переопределим файл назначения (-ov), определим размер диска (-размер 48m), имя тома (-volname NEWOS), формат файла, в котором будет закодирован диск (-формат UDTO - тот же, что используется для DVD/CD), и исходную папку, содержащую файлы, которые будут скопированы на новый диск:

hdiutil create -fs fat32 -ov -size 48m -volname NEWOS -format UDTO -srcfolder diskImage uefi.cdr

uefi.cdr готов к использованию в QEMU.

# Запуск UEFI-приложений

Как только ваш образ диска будет готов, вы можете вызвать QEMU, как показано ниже.

qemu-system-x86_64 -cpu qemu64 -bios /path/to/OVMF.fd -drive file=uefi.disk,if=ide

Когда OVMF попадет в оболочку UEFI, вы увидите дополнительную запись в "Mapping table" с пометкой "FS0". Это указывает на то, что прошивка обнаружила диск, обнаружила раздел и смогла смонтировать файловую систему. Вы можете изучить файловую систему, переключившись на нее с помощью синтаксиса в стиле DOS "FS0:", как показано ниже.

Обзор файловой системы

Вы можете запустить приложение UEFI, введя его имя.

Запуск приложения

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

Некоторые коммерческие прошивки UEFI предоставляют оболочки UEFI или возможность запуска выбранных пользователем приложений UEFI, таких как прошивка, поставляемая с линейкой ноутбуков HP EliteBook. Однако большинство из них не предоставляют эту функциональность конечному пользователю.

# Отладка

OVMF может быть построен в режиме отладки, и он будет выводить сообщения журнала на порт ввода-вывода 0x402. Вы можете использовать некоторые флаги, подобные приведенным ниже, для захвата выходных данных.

-debugcon file:uefi_debug.log -global isa-debugcon.iobase=0x402

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

# Запуск на реальном железе

# NVRAM переменные

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

Модуль ядра Linux efivarfs будет использовать эти функции для перечисления переменных NVRAM в файле /sys/firmware/efi/efivars. Переменные NVRAM также могут быть сброшены из самой оболочки UEFI с помощью команды dmpstore. Порядок загрузки устройства всегда доступен через переменные NVRAM.

# Загружаемые UEFI-приложения

Переменные NVRAM порядка загрузки определяют, где прошивка будет искать приложения UEFI, которые будут запущены при загрузке. Хотя это можно изменить (например, установщик ОС может настроить загрузочную запись для жесткого диска, на который она была установлена), прошивка обычно ищет приложение UEFI с именем "BOOT.efi" (для 32-разрядных приложений) или "BOOTX64.efi" (для 64-разрядных приложений), хранящееся в пути "/EFI/BOOT" в файловой системе загрузочного устройства. Это путь и имя по умолчанию для OVMF.

В отличие от приложения UEFI, запущенного из оболочки, если загрузочное приложение UEFI возвращает в BIOS, оно продолжит поиск других загрузочных устройств.

# Открытая функциональность

Реальные ПК различаются по объему возможностей UEFI, которые они предоставляют пользователю. Например, даже машина класса 3 может не упоминать UEFI в своей конфигурации BIOS и не предлагать оболочку UEFI. Кроме того, некоторые поставщики BIOS делают свои экраны конфигурации прошивки UEFI идентичными экранам конфигурации BIOS. Машины класса 2 могут представлять несколько запутанные меню загрузки и параметры конфигурации. Например, один производитель ноутбуков включает параметр конфигурации для включения/отключения UEFI (т.е. Переключения между поведением UEFI и CSM) под названием "OS: Windows 8". Другой ноутбук, если ему не удастся найти загрузочное приложение UEFI на выбранном загрузочном устройстве (или если это приложение вернет состояние, отличное от EFI_SUCCESS), вернется к поведению CSM, а затем пожалуется, что на диске поврежден MBR.

Чтобы упростить тестирование на реальном оборудовании, вы можете установить загрузочное приложение UEFI на внутренний жесткий диск системы, которое предоставляет меню загрузки, например rEFInd. Это также может быть удобно для сценариев с несколькими загрузками.

# Разработчики прошивки для ПК

На платформах x86 и x86-64 следующие разработчики BIOS предлагают прошивку UEFI:

  • AMI (Aptio).
  • Phoenix (SecureCore, TrustedCore, AwardCore).
  • Insyde (InsydeH20).

# Системы Apple

Системы Apple реализуют EFI 1.0, в отличие от UEFI, с тем отличием, что приложения UEFI загружаются из файловых систем HFS+ вместо FAT12/16/32. Кроме того, эти приложения UEFI должны быть "подписаны" (либо непосредственно, либо путем нахождения в подписанном каталоге) для загрузки. Blessing устанавливает флаги в файловой системе HFS+, которые проверяет прошивка Apple перед загрузкой приложения. Пакет hfsutils с открытым исходным кодом включает поддержку файлов в файловых системах HFS, но не каталогов и не HFS+.

# UEFI-приложения в деталях

# Бинарный формат

Исполняемые файлы UEFI-это обычные образы PE32 / PE32+ (Windows x32 / x64) с определенной подсистемой. Каждое приложение UEFI в основном представляет собой исполняемый файл Windows (или DLL) без таблиц символов.

Типы UEFI-образов

Тип Описание Подсистема
Приложения Загрузчики ОС и другие утилиты. 10
Драйвер службы загрузки Драйверы, используемые встроенным ПО при загрузке (например, драйверы дисков, сетевые драйверы). 11
Драйвер среды выполнения Драйверы, которые могут оставаться загруженными даже после загрузки ОС и выхода из службы загрузки. 11

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

Типы машин

Название Значение
x86 0x014c
x86_64 0x8664
Itanium x64 0x0200
UEFI Byte Code 0x0EBC
ARM 0x01C2
AArch (ARM x64) 0xAA64
RISC-V x32 0x5032
RISC-V x64 0x5064
RISC-V x128 0x5128

ARM означает, что вы можете использовать инструкции Thumb/Thumb 2, но интерфейсы UEFI находятся в режиме ARM.

# Инициализация

Приложения должны либо загрузить ОС и выйти из служб загрузки, либо вернуться из основной функции (в этом случае загрузчик будет искать следующее загружаемое приложение).

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

# Память

Карта памяти, возвращаемая UEFI, будет отмечать области памяти, используемые драйверами.

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

Типы памяти - Efi{Loader/BootServices/RuntimeServices}{Code/Data}.

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

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

Один из способов увидеть разбивку PE-файла, содержащего приложение UEFI, - это

objdump --all-headers /path/to/main.efi

Его выход довольно длинный. Среди прочего, он показывает подсистему, то есть тип образа UEFI, упомянутый ранее.

# Соглашение о вызовах

UEFI определяет следующие соглашения о вызовах:

  • cdecl для x86 UEFI-функций
  • Microsoft's 64-bit calling convention для x86-64 UEFI-функций
  • SMC для ARM UEFI-функций

Это оказывает два влияния на разработчиков приложений UEFI:

  • Основная точка входа приложения UEFI должна ожидать вызова с соответствующим соглашением о вызове.
  • Любые функции, предоставляемые UEFI, которые вызывает приложение UEFI, должны вызываться с соответствующим соглашением о вызовах.

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

# POSIX-UEFI, GNU-EFI and GCC

cdecl - это стандартное соглашение о вызовах, используемое GCC, поэтому для записи основной точки входа или вызова функций UEFI в приложении UEFI x86, разработанном с использованием GNU-EFI, не требуется никаких специальных атрибутов или модификаторов. Однако для x86-64 функция точки входа должна быть объявлена с модификатором "attribute((ms_abi))", и все вызовы функций, предоставляемых UEFI, должны выполняться через функцию "uefi_call_wrapper". Этот преобразователь вызывается с помощью cdecl, но затем преобразуется в соглашение о вызове Microsoft x86-64 перед вызовом запрошенной функции UEFI. Это необходимо, поскольку более старые версии GCC не поддерживают указание соглашений о вызовах для указателей функций.

Для POSIX-UEFI, который также использует GCC, ваша точка входа выглядит как стандартная main(), и никакого специального ABI не требуется. Кроме того, среда сборки заботится о флагах компилятора для вас, поэтому вы можете просто вызывать функции UEFI без "uefi_call_wrapper", независимо от того, используете ли вы gcc или другой кросс-компилятор.

Для удобства разработчиков как POSIX-UEFI, так и GNU-EFI предоставляют макрос "EFIAPI", который расширяется до "cdecl" при таргетинге на x86 и "attribute(ms_abi))" при таргетинге на x86-64. Кроме того, функция "uefi_call_wrapper" просто передаст вызов на x86. Это позволяет использовать один и тот же исходный код для x86 и x86-64. Например, следующая основная функция будет компилироваться с правильным соглашением о вызове как на x86, так и на x86-64, и вызов через функцию "uefi_call_wrapper" выберет правильное соглашение о вызове для использования при вызове функции UEFI (в данном случае вывод строки).

EFI_STATUS EFIAPI efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable) {
  EFI_STATUS status = uefi_call_wrapper(SystemTable->ConOut->OutputString, 2, SystemTable->ConOut, L"Hello, World!\n");
  return status;
}

# Биндинги языка

Приложения UEFI обычно пишутся на языке C, хотя биндинги могут быть написаны для любого другого языка, который компилируется в машинный код. Assembler также является опцией; для FASM доступен файл uefi.inc, который позволяет писать приложения UEFI, как показано ниже.

format pe64 dll efi
entry main
 
section '.text' code executable readable
 
include 'uefi.inc'
 
main:
    ; Инициализация библиотеки UEFI
    InitializeLib
    jc @f
 
    ; Вызов UEFI-функции для вывода на экран
    uefi_call_wrapper ConOut, OutputString, ConOut, _hello
 
@@: mov eax, EFI_SUCCESS
    retn
 
section '.data' data readable writeable
 
_hello                                  du 'Hello World',13,10,0
 
section '.reloc' fixups data discardable

Поскольку приложение UEFI содержит обычный машинный код x86 или x86-64, inline assembly также является опцией в компиляторах, которые ее поддерживают.

# EFI байткод

UEFI также включает спецификацию виртуальной машины, основанную на формате байтового кода, называемом EFI Byte Code (EBC), который может использоваться для написания независимых от платформы драйверов устройств, но не приложений UEFI. По состоянию на 2015 год использование EBC было ограниченным.

# Основные проблемы

# Мое приложение UEFI зависает/сбрасывается примерно через 5 минут

Когда управление передается вашему приложению UEFI с помощью встроенного ПО, оно устанавливает таймер на 5 минут, после чего встроенное ПО повторно активируется, поскольку предполагается, что ваше приложение зависло. Прошивка в этом случае обычно пытается сбросить систему (хотя прошивка OVMF в VirtualBox просто приводит к тому, что экран становится черным и зависает). Чтобы противодействовать этому, вам необходимо обновить таймер до истечения времени ожидания. Кроме того, вы можете полностью отключить его с помощью такого кода, как

SystemTable->BootServices->SetWatchdogTimer(0, 0, 0, NULL);

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

# Мой загрузчик зависает, если я использую определенные пользователем значения EFI_MEMORY_TYPE

Для функций управления памятью в EFI ОС должна иметься возможность использовать значения "тип памяти" выше 0x80000000 для своих собственных целей. В выпуске прошивки OVFM EFI "r11337" (для Qemu и т.д.) Есть ошибка, при которой прошивка предполагает, что тип памяти находится в диапазоне значений, определенных для собственного использования EFI, и использует тип памяти в качестве индекса массива. Конечным результатом является ошибка "array index out of bounds""; где более высокие значения типа памяти (например, разрешённые значения выше 0x80000000) приводят к сбою 64-разрядной версии прошивки (page fault) и приводят к тому, что 32-разрядная версия прошивки сообщает о неправильных значениях "attribute". Эта же ошибка также присутствует в любой версии прошивки EFI, используемой VirtualBox (которая выглядит как более старая версия OVFM); и я подозреваю (но не знаю), что ошибка может присутствовать в самых разнообразных прошивках, которые были получены из проекта TianoCore (а не только OVFM).

# Внешние ссылки