Pull to refresh

Устройство файла UEFI BIOS, часть вторая: UEFI Firmware Volume и его содержимое

Reading time9 min
Views75K
Позади уже полторы (первая, полуторная) части этой статьи, теперь наконец пришло время рассказать о структуре UEFI Firmware Volume и формате UEFI File System.

Введение


Сразу скажу, что в этой части статьи я буду описывать форматы файлов версии 2, т.к. именно они используются во всех существующих BIOS'ах. В последних версиях стандарта PI добавлено описание форматов версии 3, но они нужны для файлов размером более 16 Мб, которых пока нет ни на одной плате, хотя Gigabyte уже вплотную подобралась к этому рубежу на своих платах на Z87. Регион Descriptor на всех современных платах Intel поддерживает не более 2 микросхем емкостью не более 16 Мб, поэтому применение форматов версии 3 откладывается до очередной смены поколений чипсетов Intel, как минимум.

В качестве примера возьмем регион BIOS из файла версии 229 для Zotac Z77-ITX WiFi.
Нам понадобятся следующие программы:
  • Hex-редактор на ваш вкус, я буду использовать HxD
  • Утилита PhoenixTool v2.xx, которую можно скачать из темы на форуме MDL

Firmware Volume


Структура Firmware Volume и формат PI FFS описаны в Volume 3 документации по UEFI Platform Initializaton.
Firmware Volume — это логическое представление содержимого flash, имеющее следующие атрибуты:
  1. Name: в качестве имени FV и всех его частей выступает их GUID
  2. Size: включает в себя все данные, заголовки и свободное место
  3. Format: тип файловой системы внутри FV, различные типы имеют различные GUID'ы
  4. Allignment: требует, чтобы первый байт FV был выровнен по указанной границе, кратной степени 2. Выравнивание FV не должно быть слабее выравнивания всех файлов внутри него, кроме случая, когда в заголовке установлен флаг EFI_FVB_WEAK_ALIGNMENT. В этом случае выравнивание может быть сделано по любой степени 2, но этот FV нельзя больше перемещать
  5. Атрибуты защиты от чтения, записи, самостоятельного отключения защиты от чтения или записи
  6. OEM-атрибуты, выставляемые на усмотрение производителя

Независимо от используемой внутри FV файловой системы, заголовок FV стандартизирован и имеет следующий вид:
typedef struct {
   UINT8 ZeroVector[16];
   UINT8 FileSystemGuid[16];
   UINT64 FvLength;
   UINT32 Signature;
   UINT32 Attributes;
   UINT16 HeaderLength;
   UINT16 Checksum;
   UINT16 ExtHeaderOffset;
   UINT8 Reserved[1];
   UINT8 Revision;
   EFI_FV_BLOCK_MAP BlockMap[];
} EFI_FIRMWARE_VOLUME_HEADER;

typedef struct {
   UINT32 NumBlocks;
   UINT32 Length;
} EFI_FV_BLOCK_MAP 

ZeroVector: в начале FV зарезервировано 16 байт для совместимости с процессорами, reset vector которых находится по нулевому адресу. По присутствию чего-то отличного от нулей в этом блоке можно безошибочно выделить Boot Firmware Volume среди остальных.
FileSystemGuid: определяет используемую внутри этого FV файловую систему.
FvLength: размер FV с учетом всех заголовков.
Signature: используется для поиска FV и по стандарту всегда равна 0x4856465F, т.е. {'_','F','V','H'}.
Attributes: те самые атрибуты, о которых мы говорили выше. Их там достаточно много, но самые главные для нас это вышеупомянутый EFI_FVB_WEAK_ALIGNMENT, делающий FV неперемещаемым в случае его установки, один из набора EFI_FVB_ALIGNMENT от EFI_FVB_ALIGNMENT_1 до EFI_FVB_ALIGNMENT_2G и EFI_FVB_ERASE_POLARITY, указывающий, каким именно битом производится стирание микросхемы flash. Остальные нужны коду, который работает с FV как областью памяти, и для нас бесполезны, поэтому не станем их перечислять.
HeaderLength: размер заголовка без учета extended header'а, о котором ниже.
Checksum: 16-битная контрольная сумма заголовка. Корректный заголовок должен суммироваться в 0x0000.
ExtHeaderOffset: смещение начала extended header'а. В нем могут быть указаны GUID описываемого FV, список OEM-типов файлов вместе с их GUID'ам, а также электронная подпись. В данный момент я не встречал ни одного FV с заполненным extended header'ом, поэтому мы не будем его рассматривать. Если дополнительного заголовка у FV нет — в этом поле 0x0000.
Reserved: зарезервированное поле, всегда 0x00.
Revision: стандарт PI описывает структуру только одной ревизии — второй, поэтому в этом поле всегда 0x02.
BlockMap: карта блоков, хранящаяся в виде списка структур EFI_FV_BLOCK_MAP, заканчивающегося такой же структурой о нулями в обоих полях. Т.к. все современные микросхемы flash однородны (т.е. имеют блоки одинакового размера), то весь список состоит обычно всего из двух записей.

Проверим наш пример на соответствие вышеописанному.
Откроем наш файл BIOS'а в hex-редакторе и перейдем на смещение 0x500000, в начало региона BIOS.

Видим, что 16 нулей в начале имеются, GUID файловой системы имеется, размер этого FV — 0x020000, сигнатура на месте, атрибуты выставлены, заголовок имеет размер 0x48 байт, контрольная сумма посчитана, расширенного заголовка не имеется, зарезервированные поля на месте, а внутри этого FV имеется 20 блоков по 0x1000, что в сумме как раз и дает указанный в поле FvLength размер.
Чаще всего, в BIOS'е имеется несколько различных FV, предназначенные для разных целей, хотя это и не обязательно и можно упаковать все в один. Чемпион по степени вложенности FV и файлов друг в друга среди всех производителей UEFI BIOS'ов — Intel, там вложенность до 12 уровней встречается.
Хотя теоретически для FV могут использоваться различные файловые системы, на практике используется только одна — PI FFS, о которой мы сейчас поговорим.

Firmware File System


Это плоская файловая система без каталогов и иерархии, все файлы которой находятся в коневом каталоге. Получение списка файлов требует прохода по ФС от начала до конца. Все файлы должны иметь определенный стандартом заголовок. Файлы должны быть выровнены по восьмибайтовой границе относительно начала ФС, для выравнивания по большим границам предусмотрен специальный файл-заполнитель.
Также в стандарте описан специальный файл VTF, который обязан присутствовать в конце каждого FV, но на практике он присутствует только в конце последнего FV в образе BIOS и расположен так, что его последний байт является также последним во всей микросхеме. В нем находится код начальной загрузки, необходимой для фазы SEC.

Заголовок файла

Заголовок файла FFS устроен следующим образом:
typedef struct {
 UINT8 Name[16];
 UINT8 HeaderChecksum;
 UINT8 DataChecksum;
 UINT8 Type;
 UINT8 Attributes;
 UINT8 Size[3];
 UINT8 State;
} EFI_FFS_FILE_HEADER;

Name: GUID файла, выступающий в роли имени. В одном FV не может быть двух файлов с одинаковым GUID, если это не PAD-файлы, о которых ниже.
HeaderChecksum: восьмибитная контрольная сумма заголовка, исключая поле DataChecksum. Корректный заголовок должен суммироваться в 0x00.
DataChecksum: восьмибитная контрольная сумма содержимого файла без учета заголовка. Расчет ее требуется не всегда, а только если установлен атрибут FFS_ATTRIB_CHECKSUM, иначе это поле устанавливается в 0xAA.
Type: тип файла. Стандарт определяет 13 стандартных типов файлов (0x01 — 0x0D), 32 пользовательских типа для файлов OEM-производителей (0xC0 — 0xDF), 16 пользовательских типов для отладки (0xE0 — 0xEF) и 16 типов, специфичных для текущей версии FFS (0xF0 — 0xFF), из которых сейчас используется только 0xF0 — EFI_FV_FILETYPE_FFS_PAD для файла-заполнителя.
Этот специальный файл может иметь любой, в том числе нулевой GUID, нулевые атрибуты, стандартное состояние и любой размер. По стандарту файл обязан быть пустым, т.е. все его биты, кроме битов заголовка, должны быть установлены в значение EFI_FVB_ERASE_POLARITY. Используется он для выравнивания следующего за ним файла по границе, большей стандартных 8 байт. Минимальный размер PAD-файла равен размеру заголовка — 24 байта.
К стандартным типам файлов мы еще вернемся.
Attributes: важными для нас атрибутами являются FFS_ATTRIB_FIXED, указывающий на неперемещаемость файла внутри FV и набор FFS_ATTRIB_DATA_ALIGNMENT, указывающих на выравнивание данных (не заголовка) файла по какой-либо границе.
Size: размер файла вместе с заголовком, хранится как 24-битный UINT.
State: состояние файла. Это поле используется после загрузки FV в память и при операциях с файлами внутри FV. Состояние всех валидных файлов внутри образа BIOS — 0xF8.

Вернемся теперь к типам файлов. Тем, кто еще не читал полуторную часть этой статьи рекомендую сходить и почитать, иначе рискуете ничего не понять.
Как мы уже знаем, определено 13 стандартных типов файлов, вот они:
Название Тип Описание
RAW 0x01 Структура такого файла полностью определяется его пользователем, и о ней заранее ничего не известно
FREEFORM 0x02 Такой файл имеет секционную структуру, но о содержимом секций заранее ничего не известно
SECURITY_CORE 0x03 Ядро Security, выполняющее код в фазе SEC
PEI_CORE 0x04 Ядро PEI, оно же PEI Foundation
DXE_CORE 0x05 Ядро DXE, оно же DXE Foundation
PEIM 0x06 Модуль PEI
DRIVER 0x07 Драйвер DXE
COMBINED_PEIM_DRIVER 0x08 Гибридный модуль PEI/DXE
APPLICATION 0x09 Приложение. От драйвера DXE отличается тем, что его запускает не диспетчер DXE, а пользователь. Приложениями являются UEFI Setup, UEFI Shell, BIOS Update и т.п.
SMM 0x0A Модуль SMM
FIRMWARE_VOLUME_IMAGE 0x0B Образ FV. Это специальный файл, позволяющий вложить один FV в другой
COMBINED_SMM_DXE 0x0C Гибридный модуль SMM/DXE
SMM_CORE 0x0D Ядро SMM, оно же SMM Init

Проверим вышеуказанное на нашем файле:

Первые 0x48 байт заголовка FV нас уже не интересуют, а интересует то, что сразу за ними. Видно, что там находится файл с GUID CEF5B9A3-476D-497F-9FDC-E98143E0422C, контрольной суммой заголовка 0x36, контрольной суммой данных 0xAA, что указывает на снятый атрибут FFS_ATTRIB_CHECKSUM, типа RAW (0x01), без атрибутов, размера 0x1FFB8 и в стандартном состоянии. Похоже на правду.

Теперь о секциях. Все файлы FFS, кроме RAW, должны быть разделены на секции, выравненные по границе 4 байта от начала области данных файла. К каждому типу файлов со стороны стандарта предъявляются свои требования к количеству и типу секций, но списка требований в этой статье не будет — он слишком длинный и слишком скучный. А вот заголовки секций, их назначение и типы мы сейчас рассмотрим.

Заголовок секции

Минимальный заголовок секции выглядит так:
typedef struct {
 UINT8 Size[3];
 UINT8 Type;
} EFI_COMMON_SECTION_HEADER;

Size: размер секции в том же формате, что и в заголовке файла.
Type: тип секции.
Секции делятся на два подкласса — encapsulation и leaf. В первых могут содержатся секции других типов, а вторые содержат непосредственно данные. Секции некоторых типов имеют расширенные заголовки, но начало всегда совпадает с общим.
Для encapsulation-секций определено 3 типа содержимого:
0x01 — EFI_SECTION_COMPRESSION, указывает на то, что секция сжата по какому либо алгоритму.
Полностью заголовок EFI_COMPRESSION_SECTION (слова в названии не перепутаны, именно так) выглядит так:
typedef struct {
 UINT8 Size[3];
 UINT8 Type;
 UINT32 UncompressedSize;
 UINT8 CompressionType;
} EFI_COMPRESSION_SECTION;

UncompressedSize: размер распакованных данных.
CompressionType: применяемый алгоритм сжатия.
В данный момент встречаются 2 алгоритма сжатия — Tiano (0x01) и LZMA (0x02). Если секция была распакована без изменения структуры, то в качестве типа сжатия установлен 0x00, а размер секции совпадает с размером упакованных данных. Алгоритм Tiano — это комбинация LZ77 и кода Хаффмана за авторством Intel, код сжатия и распаковки можно взять из файлов проекта TianoCore под BSDL. Алгоритм LZMA слишком известен, чтобы рассказывать о нем еще раз, код сжатия и распаковки — в LZMA SDK.

Продолжим о типах encapsulation-секций:
0x02 — EFI_SECTION_GUID_DEFINED, указывает на то, что содержимое секции должно рассматриваться пользователем файла в соответствии с GUID, записанным в нем. Именно в такой секции могут храниться различные данные OEM, а также электронная подпись секции или всего файла.
0x03 — EFI_SECTION_DISPOSABLE, указывает на секцию, данные в которой не важны для работы файла и могут быть удалены при пересборке для экономии места. В реальных файлах BIOS не встречается.
Для leaf-секций определено 12 типов содержимого:
Название Тип Описание
PE32 0x10 64-битный исполняемый код в формате PE32+ со всеми его заголовками. Основной формат исполняемого кода в UEFI.
PIC 0x11 64-битный исполняемый код, не зависящий от позиции. Используется только некоторыми модулями PEI, формат совпадает с PE32+, только информация о relocation обрезана.
TE 0x12 64-битный исполняемый код, используемый модулями и ядром PEI. От PE32+ отличается заголовком, который уменьшен для экономии места в кэше процессора. Его описание читайте в полуторной части этой статьи.
DXE_DEPEX 0x13 Секция, описывающая зависимости драйвера DXE, в котором она находится. Формат этой секции описан в Volume 2
VERSION 0x14 Содержит версию файла и опционально Unicode-строку с полной версией. Встречается довольно редко.
USER_INTERFACE 0x15 Содержит Unicode-сроку с именем файла. Очень удобно искать файлы по содержимому этой секции. Используется часто, но встречаются и BIOS'ы без единого файла с такой секцией.
COMPATIBILITY16 0x16 16-битный исполняемый код для совместимости со старыми системами.
FIRMWARE_VOLUME_IMAGE 0x17 Секция с образом FV. Внутри этого FV может быть еще файл с такой секцией, и так далее, пока место в микросхеме не закончится.
FREEFORM_SUBTYPE_GUID 0x18 Секция содержит данные, интерпретация которых зависит от записанного ее начало GUID. Используется редко.
RAW 0x19 Секция с сырыми данными. Что с ними делать — определяет тот, кто этот файл открыл.
PEI_DEPEX 0x1B Секция, описывающая зависимости модуля PEI, в котором она находится. Формат этой секции описан в Volume 1.
SMM_DEPEX 0x1C Секция, описывающая зависимости драйвера SMM, в котором она находится, формат совпадает с DXE_DEPEX.

Проверим вышеуказанное на нашем примере. Рассматриваемый в прошлый раз файл типа RAW никаких секций не содержит, поэтому возьмем другой файл, а именно — модуль PEI по имени PchUsb, находящийся по смещению 0x7AD210:

Видно, что это действительно PEIM (тип файла — 0x06), который содержит две секции. Первая секция размера 0x3A и типа PEI_DEPEX (0x1B) содержит информацию о его зависимостях. Вторая секция (выровненная по границе 4 байта) имеет размер 0x31A и тип COMPRESSED_SECTION (0x01), а данные в ней упакованы алгоритмом LZMA (0x02) и после распаковки имеют размер 0x558. Если вы умеете распаковывать LZMA в уме, вы уже догадались, что внутри у этой секции, если же нет, я вам расскажу. Там еще 2 секции: PE32 с исполняемым кодом модуля и USER_INTERFACE с именем файла. Спрашиваете, откуда я это знаю, если не умею распаковывать LZMA в уме? There's an app for that!

PhoenixTool и заключение


Называется это приложение PhoenixTool и основная его задача — это добавление SLIC в файлы UEFI BIOS, но мы люди законопослушные и SLIC в наш BIOS добавлять не станем. Из всех функций программы нас будет интересовать кнопка Structure, доступная после указания BIOS, с которым мы будем работать. Структура открывается в отдельном окне и представляет образ UEFI BIOS'а в виде дерева. По каждому компоненту справа отображается информация о нем и доступные действия. Рассмотренный в примере выше файл в этом окне выглядит так:

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

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

Литература


UEFI Platform Initialization Specification 1.2.1 Errata A, Documents by UEFI Forum
Tags:
Hubs:
Total votes 51: ↑48 and ↓3+45
Comments4

Articles