Вы когда-нибудь задумывались, что происходит в тот момент, когда вы нажимаете кнопку питания на своем компьютере? За этой короткой паузой, перед тем как загорится экран, происходит сложная серия процессов. В этой статье мы погрузимся в увлекательный мир прошивки , исследуя, как различные компоненты взаимодействуют во время процесса загрузки .
Понимая эти связи, вы получите более ясную картину основополагающих элементов, которые оживляют вашу систему. Наше основное внимание будет уделено архитектуре Intel x86 , но многие принципы применимы и к другим архитектурам.
Если вы пропустили первую часть нашей серии, нажмите здесь , чтобы наверстать упущенное. Теперь давайте раскроем тайны, скрывающиеся за прошивкой.
Чтобы понять, как взаимодействуют компоненты прошивки, мы рассмотрим всю архитектуру со всеми ее связанными частями. Поток выполнения, показанный на схеме ниже, начинается с вектора сброса , который является частью загрузчика первой ступени . Оттуда он проходит через различные этапы прошивки:
Прошивку или BIOS обычно можно разделить на две основные части, между которыми обычно имеется минимальный интерфейс:
Дизайн прошивки платформы может быть либо монолитным , объединяющим аппаратную инициализацию и функциональность загрузки, либо он может следовать модульному и поэтапному потоку загрузки . Выбор дизайна зависит от системных требований и может быть предпочтительным для определенных устройств.
На следующей схеме показано, как взаимодействуют различные компоненты прошивки и как их можно использовать вместе для поддержки процесса загрузки (стрелки указывают последовательность выполнения):
Если эти диаграммы сейчас кажутся сложными, не волнуйтесь. Пересмотрите их еще раз после прочтения этой статьи, и они станут понятнее.
Эта часть прошивки предназначена для инициализации компьютеров и встроенных систем с упором на минимальную инициализацию оборудования : делать только то, что абсолютно необходимо, а затем передавать управление загрузчику второго этапа для загрузки операционной системы. FSBL не загружает операционные системы с носителей, отличных от флэш-чипа . Поскольку она инициализирует только базовое оборудование и не обрабатывает загрузочные носители, такие как жесткие диски, SSD или USB-флеш-накопители, для фактической загрузки операционной системы требуется другая часть программного обеспечения.
Основные обязанности FSBL :
На заре вычислительной техники программное обеспечение с открытым исходным кодом не пользовалось большой популярностью, и большинство реализаций BIOS были проприетарными. Существует лишь несколько доступных открытых решений, предоставляющих исходный код BIOS POST, например , Super PC/Turbo XT BIOS и GLaBIOS . Эти проекты были разработаны для работы на системах IBM 5150/5155/5160 и большинстве клонов XT.
Однако более известные реализации BIOS с открытым исходным кодом, такие как OpenBIOS и SeaBIOS , не выполняют инициализацию оборудования, поскольку они не предназначены для работы на голом оборудовании. Но они широко используются в качестве загрузчиков второго уровня и работают изначально в виртуальных средах, таких как QEMU и Bochs.
В любом случае, маловероятно, что вам придется работать напрямую с этими ранними BIOS или глубоко вникать в их специфику. Но если вы заинтересованы в исследовании, упомянутые репозитории являются хорошей отправной точкой.
Что касается современных тенденций развития, то, по всей видимости, разработка собственных решений BIOS не ведется, и такие проекты устарели по сравнению с современными альтернативами.
Процесс загрузки следует поэтапному потоку, начиная слева и двигаясь вправо на следующем рисунке. Временная шкала процесса загрузки платформы разделена на следующие фразы, как указано желтыми полями:
ExitBootServices()
.
Этот процесс и его фазы выполнения охвачены спецификацией UEFI Platform Initialization (PI) . Однако есть также интерфейс UEFI (обозначенный жирной синей линией на рисунке), который не является частью предыдущего документа и описан в спецификации UEFI . Хотя названия и частое использование UEFI могут сбивать с толку, эти два документа имеют разную направленность:
По сути, обе спецификации касаются интерфейсов, но на разных уровнях. Для получения подробной информации вы можете получить доступ к обеим спецификациям на сайте UEFI Forum .
UEFI PI изначально был разработан как унифицированное решение прошивки, не учитывающее различие между загрузчиками первой и второй стадии. Однако, когда мы называем UEFI загрузчиком первой стадии , он включает фазы SEC , PEI и раннюю фазу DXE . Причина, по которой мы разделяем DXE на раннюю и позднюю фазы, заключается в их различных ролях в процессе инициализации.
На ранней фазе DXE драйверы обычно выполняют необходимую инициализацию CPU/PCH/платы, а также создают архитектурные протоколы DXE (AP) , которые помогают изолировать фазу DXE от аппаратного обеспечения, специфичного для платформы. AP инкапсулируют детали, специфичные для платформы, позволяя поздней фазе DXE работать независимо от аппаратных характеристик.
Скоро появятся подробные статьи о том, как работает Coreboot. Подписывайтесь на мои социальные сети — они будут опубликованы очень скоро!
После завершения первоначальной настройки оборудования вступает в действие второй этап . Его основная роль — настроить программный интерфейс между операционной системой и прошивкой платформы , гарантируя, что ОС сможет управлять системными ресурсами и взаимодействовать с аппаратными компонентами.
SSBL стремится максимально скрыть различия в оборудовании , упрощая разработку ОС и приложений, обрабатывая большинство интерфейсов на уровне оборудования. Эта абстракция позволяет разработчикам сосредоточиться на функциональности более высокого уровня, не беспокоясь о базовых различиях в оборудовании.
Основные обязанности SSBL :
Извлечение информации о платформе : получает информацию о платформе из загрузчика первой ступени , включая отображение памяти, SMBIOS, таблицы ACPI, флэш-память SPI и т. д.
Запуск платформенно-независимых драйверов : включает драйверы для SMM, SPI, PCI, SCSI/ATA/IDE/DISK, USB, ACPI, сетевых интерфейсов и т. д.
Реализация служб (также известная как интерфейс) : предоставляет набор служб, которые облегчают взаимодействие между операционной системой и аппаратными компонентами.
Меню настройки : предлагает меню настройки конфигурации системы, позволяющее пользователям настраивать параметры, связанные с порядком загрузки, аппаратными параметрами и другими параметрами системы.
Логика загрузки : механизм поиска и загрузки полезной нагрузки (вероятно, операционной системы) с доступного загрузочного носителя.
Интерфейс в BIOS известен как BIOS services/functions/interrupt calls . Эти функции предоставляют набор процедур для доступа к оборудованию, но конкретные детали того, как они выполняются на конкретном оборудовании системы, скрыты от пользователя.
В 16-битном реальном режиме к ним можно легко получить доступ, вызвав программное прерывание с помощью инструкции языка ассемблера INT x86. В 32-битном защищенном режиме почти все службы BIOS недоступны из-за различного способа обработки значений сегментов .
Давайте возьмем, например, Disk Services ( INT 13h
), который предоставляет секторные службы чтения и записи жесткого диска и гибкого диска с использованием адресации Cylinder-Head-Sector (CHS) , в качестве примера того, как можно использовать этот интерфейс. Допустим, мы хотим прочитать 2 сектора (1024 байта) и загрузить их по адресу памяти 0x9020 , тогда можно выполнить следующий код:
mov $0x02, %ah # Set BIOS read sector routine mov $0x00, %ch # Select cylinder 0 mov $0x00, %dh # Select head 0 [has a base of 0] mov $0x02, %cl # Select sector 2 (next after the # boot sector) [has a base of 1] mov $0x02, %al # Read 2 sectors mov $0x00, %bx # Set BX general register to 0 mov %bx, %es # Set ES segment register to 0 mov $0x9020, %bx # Load sectors to ES:BX (0:0x9020) int $0x13 # Start reading from drive jmp $0x9020 # Jump to loaded code
Если вам интересно, как эта служба написана в SeaBios, взгляните на src/disk.c .
Начинает поиск загрузочного устройства (это может быть жесткий диск, CD-ROM, гибкий диск и т. д.), считывая первый 512-байтовый сектор (нулевой сектор) с устройства.
Последовательность начальной загрузки в BIOS загружает первую действительную основную загрузочную запись (MBR) , которую она находит, в физическую память компьютера по физическому адресу 0x7C00 (подсказка: 0x0000:0x7c00 и 0x7c0:0x0000 относятся к одному и тому же физическому адресу).
BIOS передает управление первым 512 байтам полезной нагрузки. Этот загруженный сектор , который слишком мал, чтобы вместить весь код полезной нагрузки, служит для загрузки остальной части полезной нагрузки с загрузочного устройства . На этом этапе полезная нагрузка может использовать интерфейс, предоставляемый BIOS.
Примечательно, что спецификации BIOS не существовали в ранние дни. BIOS является фактическим стандартом — он работает так же, как работал на реальных IBM PC в 1980-х годах. Остальные производители просто провели обратную разработку и создали IBM-совместимые BIOS. В результате не было никаких правил, запрещающих производителям BIOS изобретать новые функции BIOS или иметь дублирующиеся функциональные возможности.
Как упоминалось ранее, UEFI сам по себе является всего лишь спецификацией и имеет множество реализаций. Наиболее широко используемой является TianoCore EDK II , эталонная реализация спецификаций UEFI и PI с открытым исходным кодом . Хотя EDKII сам по себе недостаточен для создания полностью функциональной загрузочной прошивки, он обеспечивает прочную основу для большинства коммерческих решений.
Для поддержки различных загрузчиков первого этапа и предоставления интерфейса UEFI используется проект UEFI Payload . Он опирается на выполненную начальную настройку и информацию о платформе, предоставленную загрузочной прошивкой, для подготовки системы к среде UEFI.
Полезная нагрузка UEFI использует фазы DXE и BDS , которые разработаны как независимые от платформы. Она предлагает общую полезную нагрузку, которая может адаптироваться к различным платформам. В большинстве случаев она не требует какой-либо настройки или специфичных для платформы корректировок и может использоваться как есть, используя информацию о платформе из загрузчика первой ступени .
Варианты полезной нагрузки UEFI :
Legacy UEFI Payload : Требует библиотеку синтаксического анализа для извлечения необходимой информации о платформе, специфичной для реализации. Если загрузчик обновляет свой API, полезная нагрузка также должна быть обновлена.
Universal UEFI Payload : следует спецификации Universal Scalable Firmware (USF) , используя Executable and Linkable Format (ELF) или Flat Image Tree (FIT) в качестве общего формата образа. Вместо того, чтобы разбирать их самостоятельно, он ожидает получить Hand Off Blocks (HOB) при входе в payload.
В то время как Legacy UEFI Payload работает отлично, сообщество EDK2 пытается переключить отрасль на Universal UEFI Payload . Выбор между payload зависит от компонентов вашей прошивки. Например, невозможно запустить Legacy Payload с поддержкой SMM на Slim Bootloader без моего патча . С другой стороны, использование Universal Payload с coreboot требует слоя shim для перевода таблиц coreboot в HOBs , функция доступна только в форке StarLabs EDK2 .
Каждая UEFI-совместимая система предоставляет системную таблицу , которая передается каждому коду, работающему в среде UEFI (драйверы, приложения, загрузчики ОС). Эта структура данных позволяет исполняемому файлу UEFI получать доступ к таблицам конфигурации системы , таким как ACPI , SMBIOS и набору служб UEFI .
Структура таблицы описана в MdePkg/Include/Uefi/UefiSpec.h :
typedef struct { EFI_TABLE_HEADER Hdr; CHAR16 *FirmwareVendor; UINT32 FirmwareRevision; EFI_HANDLE ConsoleInHandle; EFI_SIMPLE_TEXT_INPUT_PROTOCOL *ConIn; EFI_HANDLE ConsoleOutHandle; EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut; EFI_HANDLE StandardErrorHandle; EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *StdErr; // // A pointer to the EFI Runtime Services Table. // EFI_RUNTIME_SERVICES *RuntimeServices; // // A pointer to the EFI Boot Services Table. // EFI_BOOT_SERVICES *BootServices; UINTN NumberOfTableEntries; EFI_CONFIGURATION_TABLE *ConfigurationTable; } EFI_SYSTEM_TABLE;
Службы включают в себя следующие типы: службы загрузки , службы времени выполнения и службы, предоставляемые протоколами .
UEFI абстрагирует доступ к устройству, настраивая протоколы UEFI . Эти протоколы представляют собой структуры данных, содержащие указатели функций , и идентифицируются глобально уникальным идентификатором (GUID) , который позволяет другим модулям находить и использовать их. Их можно обнаружить через службы загрузки.
Драйвер UEFI создает эти протоколы, а фактические функции (не указатели!) содержатся в самом драйвере. Этот механизм позволяет различным компонентам в среде UEFI взаимодействовать друг с другом и гарантирует, что ОС может взаимодействовать с устройствами до загрузки собственных драйверов.
Хотя некоторые протоколы предопределены и описаны в спецификации UEFI, поставщики встроенного ПО также могут создавать свои собственные пользовательские протоколы для расширения функциональности платформы.
Загрузочные службы
Предоставляют функции, которые могут использоваться только во время загрузки. Эти службы остаются доступными до тех пор, пока не будет вызвана функция EFI_BOOT_SERVICES.ExitBootServices()
( MdeModulePkg/Core/Dxe/DxeMain/DxeMain.c ).
Указатели на все службы загрузки хранятся в таблице служб загрузки ( MdePkg/Include/Uefi/UefiSpec.h ):
typedef struct { EFI_TABLE_HEADER Hdr; ... EFI_GET_MEMORY_MAP GetMemoryMap; EFI_ALLOCATE_POOL AllocatePool; EFI_FREE_POOL FreePool; ... EFI_HANDLE_PROTOCOL HandleProtocol; ... EFI_EXIT_BOOT_SERVICES ExitBootServices; ... } EFI_BOOT_SERVICES;
Службы времени выполнения
Минимальный набор служб все еще доступен, пока работает операционная система. В отличие от служб загрузки, эти службы остаются действительными после того, как любая полезная нагрузка (например, загрузчик ОС) взяла под контроль платформу через вызов EFI_BOOT_SERVICES.ExitBootServices()
.
Указатели на все службы времени выполнения хранятся в таблице служб времени выполнения ( MdePkg/Include/Uefi/UefiSpec.h ):
typedef struct { EFI_TABLE_HEADER Hdr; ... EFI_GET_TIME GetTime; EFI_SET_TIME SetTime; ... EFI_GET_VARIABLE GetVariable; EFI_GET_NEXT_VARIABLE_NAME GetNextVariableName; EFI_SET_VARIABLE SetVariable; ... EFI_GET_NEXT_HIGH_MONO_COUNT GetNextHighMonotonicCount; EFI_RESET_SYSTEM ResetSystem; ... } EFI_RUNTIME_SERVICES;
На рисунке ниже показана временная шкала для служб загрузки и выполнения, поэтому вы можете точно видеть, когда каждая из них активна.
Спецификация UEFI определяет механизм политики загрузки, называемый менеджером загрузки UEFI . Он попытается загрузить приложения UEFI в определенном порядке. Этот порядок и другие параметры можно настроить, изменив глобальные переменные NVRAM (энергонезависимая память с произвольным доступом) . Давайте обсудим самые важные из них:
Boot####
( ####
заменяется уникальным шестнадцатеричным значением) — параметр загрузки/загрузки.BootCurrent
— параметр загрузки, используемый для запуска текущей работающей системы.BootNext
— параметр загрузки только для следующей загрузки. Заменяет BootOrder
только для одной загрузки и удаляется менеджером загрузки после первого использования. Позволяет изменить поведение следующей загрузки без изменения BootOrder
.BootOrder
— упорядоченный список загрузки вариантов загрузки. Менеджер загрузки пытается загрузить первый активный вариант в этом списке. Если не удается, он пробует следующий вариант и т. д.BootOptionSupport
— типы параметров загрузки, поддерживаемые менеджером загрузки.Timeout
— тайм-аут менеджеров загрузки прошивки в секундах перед автоматическим выбором значения запуска из BootNext
или BootOrder
.
Эти переменные можно легко получить из Linux с помощью efibootmgr(8) :
[root@localhost ~]# efibootmgr BootCurrent: 0000 Timeout: 5 seconds BootOrder: 0000,0001,2001,2002,2003 Boot0000* ARCHLINUX HD(5,GPT,d03ca3cf-1511-d94e-8400-c7a125866442,0x40164000,0x100000)/File(\EFI\ARCHLINUX\grubx64.efi) Boot0001* Windows Boot Manager HD(1,GPT,6f185443-09fc-4f15-afdf-01c523565e52,0x800,0x32000)/File(\EFI\Microsoft\Boot\bootmgfw.efi)57a94e544f5753000100000088900100780000004200430044039f0a42004a004500430054003d007b00390064006500610038003600320063002d1139006300640064002d0034006500370030102d0061006300630031002d006600330032006200330034003400640034003700390035007d00000033000300000710000000040000007fff0400 Boot0002* ARCHLINUX HD(5,GPT,d03ca3cf-1511-d94e-8400-c7a125866442,0x40164000,0x100000) Boot2001* EFI USB Device RC Boot2002* EFI DVD/CDROM RC Boot2003* EFI Network RC
Давайте рассмотрим загрузку, опираясь на фрагмент кода выше. UEFI начнет итерацию списка BootOrder
. Для каждой записи в списке он ищет соответствующую переменную Boot####
— Boot0000
для 0000, Boot2003
для 2003 и т. д. Если переменная не существует, он переходит к следующей записи. Если переменная существует, он считывает содержимое переменной. Каждая переменная параметра загрузки содержит дескриптор EFI_LOAD_OPTION
, который представляет собой байтовый буфер полей переменной длины (это просто структура данных).
Структура данных описана в [MdePkg/Include/Uefi/UefiSpec.h][ https://github.com/tianocore/edk2/blob/edk2-stable202405/MdePkg/Include/Uefi/UefiSpec.h#L2122 )
typedef struct _EFI_LOAD_OPTION { /// The attributes for this load option entry. UINT32 Attributes; /// Length in bytes of the FilePathList. UINT16 FilePathListLength; /// The user readable description for the load option. /// Example: 'ARCHLINUX' / 'Windows Boot Manager' / `EFI USB Device` // CHAR16 Description[]; /// A packed array of UEFI device paths. /// Example: 'HD(5,GPT,d03ca3cf-1511-d94e-8400-c7a125866442,0x40164000,0x100000)/File(\EFI\ARCHLINUX\grubx64.efi)' // EFI_DEVICE_PATH_PROTOCOL FilePathList[]; /// The remaining bytes in the load option descriptor are a binary data buffer that is passed to the loaded image. /// Example: '57a9...0400' in Boot0001 variable // UINT8 OptionalData[]; } EFI_LOAD_OPTION;
На этом этапе прошивка проверит путь к устройству ( EFI_DEVICE_PATH_PROTOCOL ). В большинстве случаев наш компьютер загружается с устройства хранения данных (жесткий диск/SSD/NVMe и т. д.). Таким образом, путь к устройству будет содержать узел HD(Partition Number, Type, Signature, Start sector, Size in sectors)
.
Примечание : Если вам интересно, как преобразовать другие пути, прочтите спецификацию UEFI v2.10, 10.6.1.6 Справочник по узлу текстового устройства .
UEFI проверит диск и определит, есть ли на нем раздел, соответствующий узлу. Если он существует, он должен быть помечен определенным Глобальным уникальным идентификатором (GUID) , который отмечает его как раздел системы EFI (ESP) . Этот раздел отформатирован с файловой системой, спецификация которой основана на конкретной версии файловой системы FAT и называется EFI File System ; на самом деле, это просто обычная FAT12/16/32 .
File(\Path\To\The\File.efi)
, то UEFI будет искать этот конкретный файл. Например, параметр Boot0000
содержит File(\EFI\ARCHLINUX\grubx64.efi)
.\EFI\BOOT\BOOT{arch}.EFI
( BOOTx64.EFI
для amd64 или BOOTia32.EFI
для i386 / IA32 ). Этот механизм позволяет загрузочному съемному носителю (например, USB-накопителю) работать в UEFI; они просто используют резервный путь загрузки . Например, опция Boot0002
будет использовать этот механизм.
Примечание: Все параметры Boot####
, упомянутые выше, относятся к параметрам загрузки, отображаемым в примере вывода efibootmgr .
В обоих случаях диспетчер загрузки UEFI загрузит приложение UEFI (это может быть загрузчик ОС , оболочка UEFI, служебное программное обеспечение, настройка системы и т. д.) в память. В этот момент управление передается точке входа приложения UEFI . В отличие от BIOS , приложение UEFI может вернуть управление прошивке (кроме ситуации, когда приложение берет на себя управление системой). Если это происходит или что-то идет не так, диспетчер загрузки переходит к следующей записи Boot####
и следует точно такому же процессу.
В спецификации упоминается, что менеджер загрузки может автоматически поддерживать переменные базы данных. Это включает удаление переменных опций загрузки, на которые нет ссылок или которые не могут быть проанализированы. Кроме того, он может переписать любой упорядоченный список, чтобы удалить любые опции загрузки без соответствующих переменных опций загрузки.
Вышеприведенный текст описывает загрузку UEFI . Кроме того, прошивка UEFI может работать в режиме Compatibility Support Module (CSM) , который эмулирует BIOS.
Часть программного обеспечения, запускаемая прошивкой (обычно загрузчиком второго уровня ) и использующая ее интерфейс для загрузки ядра ОС . Может быть таким же сложным, как ОС, предлагая такие функции, как:
Общие конструкции этих программ выходят за рамки этой статьи. Для подробного сравнения популярных загрузчиков ОС вы можете обратиться к вики ArchLinux и статье в Википедии .
Система Windows использует собственный загрузчик ОС, известный как диспетчер загрузки Windows (BOOTMGR) .
Прошивка больше не является маленьким, сложным фрагментом кода. Она превратилась в огромный объем сложного кода , и современные тенденции только способствуют этому. На ней можно запустить Doom , Twitter и множество других интересных приложений.
Понимание общей архитектуры помогает организовать эти компоненты в вашем сознании. Изучая дизайн существующей прошивки, вы получаете представление об увлекательном процессе, который разворачивается каждый раз при включении компьютера. Эта нисходящая перспектива не только проясняет роль каждой части, но и подчеркивает сложную и развивающуюся природу современных систем прошивки.