Bootx64 efi что это
Перейти к содержимому

Bootx64 efi что это

  • автор:

Welcome to the Mike’s homepage!

Various things I do outside of work: fun, boring, anything really.

Getting started with EFI

by Mike Krinkin

I’m trying to explore another relatively new are for me: UEFI. When working on student and hobbt project many people tend to start from legacy BIOS or multiboot to boot their hello world kernels.

On the one hand it makes a lot of sense to use the simplest solution possible. On the other hand EFI complexity serves some purpose and with EFI you get a lot of useful tools right out of the box.

With all that in mind let’s try to cook up something with EFI. All the sources are available on GitHub.

The basics

EFI has a publicly available specificaion that should serve as a reference: UEFI Specification. At the moment of writing the latest specification version available is 2.8.

The document is rather long, so reading through all of it is a bit inefficient. Before diving right into, it might be useful to set straight some basics:

  • EFI code is expected to use Microsoft ABI at least for all interaction with the firmware
  • EFI code is expected to be position independent as there is no guarantees that it will be loaded at a fixed address
  • The EFI binary format is PE32+ (don’t get confused by the 32 in the name, it doesn’t mean that it’s limited to just 32 bit code)

So it looks like what we need is to build a Windows binary, which was part of the problem with using it for various hobby and student projects. It wasn’t impossible, but building an EFI application used to require all kinds of hacky solutions, like GNU EFI, or wrapping a normal ELF binary into something that looks like PE32+ executable, like Linux EFI stub.

Fortunately LLVM and Clang projects got quite mature and LLVM/Clang supports building EFI binaries, so it’s much easier now.


Now, let’s install the tools we’d need to build and test our hello world example.

I will be using QEMU for testing and OVMF is the UEFI firmware that QEMU can use.

We will use Clang for compilation and lld is a linker developed inside the LLVM ecosystem. We need lld because it supports generating PE32+ binaries.

I will keep my code and data within ws/efi directory, so let’s prepare that as well:

OVMF.fd file contains the image of the UEFI firmware that QEMU can use. I think it’s supposed to be some kind of flash image and flash should contain both the code and some configuration parameters, so I would expect that QEMU might need to write something there. That’s why I copied the image to the working directory.

The root subdirectory is going to represent the fake filesystem that we will use inside our virtual machine. Basically we will put our EFI binary there to make it available inside QEMU.

And we all done.

EFI programming model

The complete EFI programming model is rather rich and supports a lot of different usecases, but we just want a hello world, so figuring out all of that is a bit wasteful. So we concentrate on the simple EFI applications.

We start from looking at the environment in which our application will execute. As mentioned before we don’t know exactly what address the application will be loaded at for sure. However we do know that it will run in long mode for 64 bit binaries and in uniprocessor mode, even if there are multiple cores available.

Additionally paging will be enabled, but all relevant addresses are identity mapped. Whcih means that memory mapped such that virtual address matches with the physical address.

There are also other details, like how much stack we have and values of various control bits. You can find more in the section 2.3.2 IA-32 Platforms of the UEFI Specification. I will not go into all the details of the hardware state, because at this point a high level understanding is enough.

EFI application entry point gets two parameters EFI_HANDLE and EFI_SYSTEM_TABLE pointer in registers RCX and RDX correspondingly. I myself didn’t yet figure out what the EFI_HANDLE is needed for, but EFI_SYSTEM_TABLE pointer is quite important.

As I mentioned EFI provides us with a bunch of useful tools out of the box and this EFI_SYSTEM_TABLE pointer is how you can access those tools. Basically this table (indirectly) contains a bunch of function pointers, that can be used to call into EFI to request the firmware to do something. For example, you can ask EFI to print something on the screen/serial port/whatever makes sense for your platform or you can ask it to load some data from the filesystem in memory.

NOTE: RCX and RDX is not how you pass arguments to a function in System V ABI for x64 architecture, that’s how you pass arguments to a function in Microsoft ABI. UEFI Specification is more or less self contained and it does describe the calling convention it uses, so you don’t need a separate document for that.

EFI Hello World

So far I’ve been a bit hand wavy, so it’s time to get to the specifics and write some code. I will start from defining required data types, but I will cheat a bit and will only define the parts I’m actually going to use.

I’ll start with the EFI_HANDLE and EFI_SYSTEM_TABLE that our entry point takes as parameters. EFI_HANDLE is one of the basic data types in the UEFI Specification and it’s basically a generic data pointer, or in C terms it’s void * . The complete list of basic data types is available in the section 2.3.1 Data Type of the UEFI Specification.

EFI_SYSTEM_TABLE is a structure as you could have guessed already. It’s defined in the section 4.3 EFI System Table of the [UEFI Specifcation]. It starts with a header. The content of the header is not really important for us because I’m not going to use it, but we need to get the size right, so I will provide the complete definition:

With this header in place this is how I’ll define the system table for now:

Huh, not a very useful structure at this point. Well, it’s because I’m implementing a simple hello world, so we don’t really need much from the EFI. As a result most of the fields there are unused at the moment, so I’ve just put placeholders there.

There is one field that I populated however and it’s out . I will describe the efi_simple_text_output_protocol structure shortly, but as a name suggest it has something to do with putting some text out, which is useful if we want to create a simple hello world.

As you can see the structure consists mostly of pointers to functions and that’s how you interact with EFI. The complete definition can be found in the section 12.4 Simple Text Output Protocol of the UEFI Specification.

As before most of the functions are of no interest to us at the moment, so I’ve only named two functions: output_string and clear_screen .

The names of the functions should be self explanatory, however we should stop a little bit on why the output_string function takes a pointer to uint16_t * instead of something like const char * .

Let’s start from the const part missing. It appears to me that for whatever reason the creators of the UEFI Specification just didn’t bother to put any const qualifiers into the specification at all, so lack of const doesn’t actually mean anything.

The uint16_t part is more interesting. In the specification they use CHAR16 type instead of uint16_t , which is defined as 2 byte character type. It seems that accross the UEFI Specification they mostly use UCS-2 encoded strings. Why they don’t use something like UTF-8 or UTF-16 is not clear to me. I guess we will have to live without Egyptian hieroglyphs and emojis for now, all the basic latin, cyrillic and CJK characters are supported though.

With that out of the way, we can now write our hello world example:


Now to the compilation and linking of the code. In LLVM ecosystem code is initially translated into an intermediate byte code that is later translated into machine code. That’s not a novel idea even GCC is doing something very similar.

We are interested in configuring the second step, the one that is responsible for translating the intermediate byte code into the actual machine code, that in the end we want to get a position independent code that follows microsoft ABI.

It appears that the only thing we need to do is to specify LLVM the right target platform via -target=x86_64-unknown-windows parameter. In addition to that a couple of additional parameters might be needed: -ffreestanding and -mno-red-zone .

NOTE: it appears that we don’t even need to tell LLVM separately that the code should be position independent, moreover parameters like -fpic aren’t even recognized with the windows backend.

ffreestanding flag tells the compiler to compile the code for the freestanding environment, as opposed to the hosted environment. What freestanding environment means exactly according to language standards is to a significant extent implementation defined. That being said, in practice it tells the compiler that it cannot depend on the availability of many common library functions, like, for example, memory allocation functions. If the code will not have access to common OS services, like in our use case, you should specify this flag.

mno-red-zone is a x64 specific flag. It disables so called red zone, which is in essense a form of optimization. You can read in more details about it in the Eli Bendersky article. This optimization doesn’t quite work well with interrupt handling in freestanding environment, so for most of the kernel/firmware code it should be disabled.

So compilation is not really that tricky, so let’s move to linking. First, let me remind you that earlier we installed the lld linker. That’s because we need a linker capable of generating a PE32+ binary. lld with the flag -flavor link can do that.

Additionally we will have to tell the linker that we want to create an EFI application. Format PE32+ supports a few different types of executables/libraries, so we need to tell the linker that what we need in the end is not a regular Windows executable or DLL, but an EFI application. That can be done with -subsystem:efi_application flag.

Every executable has to have an etnry point. Normally, the entry point is known to be a function with a specific name (like, main), so you don’t need to explicitly specify it. In our case however, this convention doesn’t work and we need to explicitly tell the linker what is our entry point using -entry:efi_main flag.

NOTE: you may find other examples on the Internet that also pass -dll flag. This flag tells the linker that they need to generate a DLL, which makes sense if the binary can be located anywhere in memory, but I found that this flag makes little difference and the code still works without it.

Here is how my Makefile ended up looking:

When the building finishes it should produce a file name bootx64.efi . We can look at the file headers using objdump to check it’s format:

NOTE: We can additionally disassemble the code in the file to make sure that it’s position independent. However there isn’t much code in the file to begin with and none of the code there will benefit in any way from being position dependent anyway. In other words, even if my building procedure isn’t quite correct, we’d hardly will be able to see it on this simple example.


Now it’s time to try and run the binary inside the QEMU. We will start from copying the binary to the root directory of the virtual machine:

NOTE: at this point we can actually use any path inside the root directory, so there is nothing magical about efi/boot specifically at this point.

To start the QEMU we need a command similar to this:

Most important options are the two -drive options. In general drive option provide QEMU with information about various storage devices it need to emulate, where storage is a rather broad term in this case.

-drive if=pflash,format=raw,file=/home/kmu/ws/efi/OVMF.fd option tells QEMU to emulate a some kind of flash memory with content from the file /home/kmu/ws/efi/OVMF.fd . The OVMF.fd file contains the image of the UEFI firmaware that we installed earlier.

-drive format=raw,file=fat:rw:root option tells QEMU to simulate a disk-like device that contains a readable and writable FAT filesystem. The interesting thing is that it will use the root directory on the host computer as filesystem storage, which makes it a rather convenient way to share state between the host and the virtual machine.

The rest of the options are less relevant. I use the -net none to tell QEMU that there is no network and that firmware should not try to boot the system over network, but things should work even without this option. -nographic option just disables the graphical screen and all the input/output will be done via terminal. I do that because I often work without a mouse and it’s more convenient to interract via terminal then use touchpad.

The command above should start the virtual machine and eventually drop you into the EFI shell. It will not start our EFI application automatically, so we need to do that ourselves.

In the shell type this:

The first command selects fs0 volume. You need to do that before you can perform any filesystem operations. cd efi/boot command is more or less self explanatory. And the last command just starts the binary.

Our application clears the screen, outputs a string and exits. That should happen pretty quickly, so you may not notice anything, except that the screen has been cleared. You may want to change the code a little bit to put an infinite loop before the last return in the efi_main to see the results of your work.

NOTE: a tip, backspace button doesn’t work with -nographic , instead you shdoul use CTRL + H combination.

Instead of conclusion

A hello world application is hardly anything to be proud of, but there is a lot happening under the hood. It might look easy now, when Clang/LLVM supports generating EFI binaries out of the box, but it wasn’t always the case. So appreciate and enjoy the good tooling available nowdays.


Статья сфокусирована на объяснении понятий и принципов, а также важных и интересных возможностях UEFI. Более детально сам процесс установки и настройки загрузки Ubuntu в UEFI режиме описан в этой статье или в этой.

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

Старый добрый BIOS отвечает за первичное тестирование (POST), инициализацию практически всего аппаратного обеспечения компьютера и за инициализацию загрузки ОС с дискового носителя (хотя сегодня уже не все носители имеют в своей конструкции диск ). Собственно вся процедура инициализации загрузки ОС заключалась в двух шагах:

За все остальное (даже за работу с таблицей разделов, которая располагается в конце MBR) отвечает тот самый маленький код, который расположен в первых 446 байтах MBR.

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

Другой проблемой являются ограничения, которые установлены для поддержки BIOS: это 16-разрядный реальный режим работы процессора с набором команд i8086, 1Мб адресуемого пространства памяти и периферия (клавиатура, видео адаптер, контроллер прямого доступа в память) совместимая с IBM AT. На сегодняшний день, требовать от 64-х разрядного многоядерного, многопоточного процессора совместимости с одноядерным и однопоточным 16-разрядным i8086 — уже немного смешно, а требование совместимости по периферии с IBM PC-AT(компьютер 1984-го года выпуска) уже даже не смешно, а очень грустно.

Идеи отказаться от всего того ненужного, что делает BIOS, снять архаичные ограничения BIOS и сделать процесс инициализации и загрузки более гибким возникали уже очень давно, и различные попытки сделать это предпринимались, но IT-индустрия реально созрела к принятию нового общего стандарта загрузки персональных компьютеров только в начале этого века. В 2005 был создан консорциуму UEFI Forum, которому INTEL передал свою наработки по проекту Intel Boot Initiative (позже переименованному в EFI — Extensible Firmware Interface), начатому еще середине 90-х. Помимо Intel в консорциум вошли AMD, Apple, IBM, Microsoft и многие другие крупные IT-компании. Вместе с созданием консорциума спецификация EFI была переименована в UEFI (Unified Extensible Firmware Interface) 1) .

Основные концепции положенные в основу UEFI — «минималистичность», «модульность» и поддержка разных процессоров. Прошивка UEFI может быть собрана под 32-битный или 64-битный процессоры Intel, 32- или 64-битный процессор ARM, а также для процессоров Intel Itanium. Кроме того UEFI имеет собственный менеджер загрузки и умеет работать с файловыми системами на диске (по умолчанию только FAT32), а также умеет загружать драйверы для поддержки различного оборудования и любых файловых систем.
После включения компьютера и проведения первичного теста оборудования (на этом этапе разницы с BIOS нет) инициализируются только те подсистемы, которые необходимые для загрузки.

В некоторых режимах загрузки, которые обычно называют Fast-boot (или Fast-Startup), даже клавиатура может оставаться не инициализированной, пока не загрузится ОС. Некоторые настройки Fast-boot могут отключать работу со всеми USB устройствами и не будет возможности даже выбрать с чего грузиться компьютеру, ведь клавиатура не работает до тех пор, пока не загрузится ОС.

Причем, в отличии от BIOS, все компоненты которого записаны в постоянную (флеш) память, UEFI может хранить часть своего кода на диске — в специальном разделе ESP 2) . Это позволяет легко расширять, менять и обновлять код поддерживающий различные устройства компьютера без необходимости перепрограммировать постоянное ЗУ компьютера. А кроме драйверов для поддержки устройств на ESP разделе может хранится и поддержка протоколов, утилиты и даже собственная EFI-оболочка (shell), из-под которой можно запускать собственные EFI-приложения. Т.е. UEFI по сути — маленькая ОС (или псевдо-ОС). При этом загрузчик операционной системы становится обычным приложением, которое исполняется в окружении, предоставляемом псевдо-ОС UEFI. Подробнее о EFS разделе мы поговорим в главе ESP раздел.

UEFI реализует свой менеджер загрузки, он поддерживает мульти-загрузку: позволяет выбирать загрузку из нескольких ОС и/или утилит. Загрузчик ОС — это просто efi-приложение, которое хранится на ESP разделе. В ESP может храниться много приложений, и пункты меню загрузки могут ссылаться каждый на свое приложение (или на одно, но с передачей разных параметров). Кроме того, UEFI поддерживает ассоциацию нажатия клавиш с пунктами загрузки (выбор пункта загрузки осуществляется в зависимости от того какая нажата клавиша в момент загрузки). Т.о. UEFI берет на себя большую часть задач, которые раньше могли решать только загрузчики ОС (типа GRUB). Более подробно мы поговорим о менеджере загрузки UEFI в разделе Менеджер загрузки UEFI

Архитектура UEFI позволяет также загрузить драйвера устройств (необходимые для работы ОС и для процедуры загрузки). Да и само ядро ОС можно загрузить (по крайней мере ядро Linux, собранное с опцией UEFISTUB) непосредственно из UEFI (т.е. устранить загрузчик из процесса загрузки). Такую возможность мы разберем более детально чуть позже в разделе Загрузка ядра Linux непосредственно из UEFI.

Как и в случае с BIOS, UEFI — это стандарт, практической реализацией которого занимаются разные поставщики решений. Поэтому в реализации стандарта UEFI в прошивках разных поставщиков могут значительно отличаться (как визуально так и функциональным наполнением). Могут встречаться сильно урезанные по функционалу реализации (чаще в компьютерах выпущенных в 2000-х годах). Но базовые принципы работы UEFI в процессе загрузки различаться сильно не должны (он был определен и зафиксирован в самых ранних стандартах UEFI). Интерфейсы утилиты настройки могут сильно различаться, но значение имеет не способ оформления интерфейса, а те настройки, которые доступны для изменения через этот интерфейс.


Само собой, современные прошивки умеют эмулировать работу BIOS. Отвечает за совместимость с BIOS модуль CSM (Compatibility Support Module иногда он еще называется Legasy support). Когда CSM включен и при загрузке загрузиться в UEFI не удалось, то CSM пытается найти загрузчик ОС в первом секторе диска (MBR) или в специальном служебном разделе (при GPT разбивке диска). Если код загрузчика найден, то CSM считает, что установлена ОС, требующая поддержки режима BIOS, и, прежде чем загрузить и запустить код из MBR, CSM проделывает все те же шаги инициализации оборудования, что предусмотрены для BIOS.

Если вы планируете использовать только загрузку в варианте UEFI, то работу этого модуля лучше запретить в системной утилите вашей материнской платы (Firmware setup utility). Кроме того, загрузка в режиме SecureBoot (о ней будет рассказано ниже) явно требует, что бы работа модуля CSM была запрещена.

ESP раздел

Отдельно стоит рассмотреть служебный раздел UEFI. Раздел обязательно должен иметь специальный идентификатор UUID = C12A7328-F81F-11D2-BA4B-00A0C93EC93B (или тип EF если он создан на диске с таблицей разделов в MBR). По стандарту этот раздел должен иметь файловую систему FAT32 (в принципе стандарт допускает использование других ФС, но на практике это не распространено). Предусмотренная в стандарте структура каталогов довольно проста.

При установке UBUNTU (с загрузкой через UEFI) служебный раздел ESP монтируется в /boot/efi. И на ESP разделе создается каталог EFI\ubuntu в котором размещается загрузчик 4) GRUB и/или Shim (о них — чуть позже).


Еще одно «новшество» в индустрии относится к таблице разделов диска.

Во времена IBM-PC/AT и BIOS таблица разделов размещалась в первом секторе на диске, т.е. в MBR (Master Boot Record). В MBR хранится и исполняемый код загрузчика ОС и таблица разделов. Т.к. в 512 5) байт много не запихнешь, то таблица разделов была рассчитана только на 4 раздела. Изначальное ограничение на 4 раздела позже было обойдено через внедрение расширенного раздела — внутри него можно было создать множество логических разделов 6) .

Другой недостаток 7) : на указатели начала и конца раздела было зарезервировано 32 бита, что позволяло адресовать 2 терабайта (что в те давние времена казалось просто астрономически много ).

Однако уже к началу 2010-х на рынке стали появляться диски более 2Тб. Но к этому времени уже набрал популярность стандарт таблицы разделов GPT (GUID Partition Table). В GPT число разделов практически не ограничено (по умолчанию 128, но можно и больше) и установлена новая планка для размера диска: 9,4 ЗБ, т.е 9,4 × 10^21 байт (на сегодня этот размер вновь выглядит астрономически большим… )

К чему я упомянул про таблицы разбиения дисков? А к тому, что, несмотря на то, что первоначально UEFI планировалось использовать только с GPT, а BIOS умел работать с MBR (вернее код в MBR работает с таблицей разделов расположенной в конце MBR), но в реальной жизни и BIOS (вернее сказать CSM) научили понимать GPT, и для UEFI предусмотрели использование таблицы разделов из MBR.

Получившаяся в результате «солянка» (из 4-х допустимых вариантов: UEFI + MBR, UEFI + GPT, BIOS/CSM + MBR и BIOS/CSM + GPT) создает некоторую путаницу и недопонимание. Давайте попробуем во всем этом разобраться более детально.

Что нужно для того что бы ОС грузилась через BIOS?

Несмотря на то, что загрузка в режиме BIOS уже не современна, не модна , а главное — технологически устарела, но, уверен, будет еще много сторонников этого пути, на протяжении долгих лет… Однако я не могу найти ни одного разумного довода использовать этот вариант если и ОС и компьютер поддерживают загрузку через UEFI.

В первую очередь, для загрузки через BIOS, в утилите настройки вашего компьютера должна быть разрешена работа модуля CSM. Само собой, режим SecureBoot (о нем рассказано чуть ниже) в таком варианте организовать невозможно в принципе.

Если разбивка диска — c таблицей разделов в MBR (вариант «BIOS/CSM + MBR»), то ничего дополнительно не нужно (это сочетание собственно и есть оригинальная конфигурация для варианта загрузки через BIOS).

А вот если у вас диск размечен через GPT (вариант «BIOS/SCM + GPT»), то возникают некоторые сложности.
В первую очередь: не совсем ясно — куда разместить код, который ранее размещался в MBR. Да, в GPT первый блок размером в 512 байт зарезервирован и в нем даже есть защитная версия таблицы разделов (в формате принятом для MBR). В защитной таблице разделов записан единственный раздел размером на все адресуемое пространство диска 8) и с типом EE. Само собой, с единственной фейковой записью в таблице разделов никакой код в MBR не сможет корректно отработать процесс загрузки 9) .
Еще одно проблемное место возникает когда загрузчик использует для размещения своего кода сектора на первом треке диска за MBR (к примеру так поступает GRUB). В GPT на этом месте уже идут служебные записи (заголовок таблицы разделов и сами записи о разделах).
Для решения обоих этих проблем предусмотрен специальный бинарный (он не форматируется ни в какую ФС) раздел BIOS-Boot (Тип = EF02 или UUID = 21686148-6449-6E6F-744E-656564454649). Установщик ОС может прописать на этот раздел весь тот код, который ранее размещался в MBR и на незанятом пространстве за MBR. Этот раздел должен иметь установленный флаг boot. Размер этого раздела может быть совсем маленьким, ведь MBR — это всего 512 байт, а размер неиспользуемого пространства за MBR, составляет всего 31Кb или 1023,5Кb 10) . Однако, некоторые современные загрузчики требуют раздел BIOS-Boot размером в 10-30Мb.
В процессе загрузки CSM модуль (эмулирующий работу BIOS) отвечает за то, что находит в GPT раздел BIOS-Boot, загружает первые 512 байт с этого раздела, и передает управление загруженному коду. За дальнейший процесс загрузки отвечает этот код, а CSM берет на себя задачу эмулировать обращение к секторам диска за MBR (вместо этих секторов производится подстановка секторов из раздела BIOS-Boot).

Пожалуй вот на этом и закончим обсуждать CSM и BIOS, все дальнейшее будет относится только к UEFI.

Что нужно для установки ОС, что бы она грузилась через UEFI?

На самом деле все не так сложно как кажется. Как уже было сказано UEFI требует специального раздела ESP (с файловой системой FAT32 и флагами boot и esp). И, несмотря на то, что для этого раздела был предусмотрен специальный длинный идентификатор (UUID = C12A7328-F81F-11D2-BA4B-00A0C93EC93B) для GPT, который в таблицу разделов MBR ну никак не запихнешь, этому разделу дали еще и короткий идентификатор (EF), который можно «впихнуть» в таблицу разделов в MBR. Т.е. ESP раздел можно создать и на диске с GPT и на диске с таблицей разделов в MBR (однако такой вариант поддерживает не любая ОС).

Т.о. при установке системы, помимо создания нужных для работы ОС разделов, нужно создать специальный раздел ESP:

— для варианта «UEFI + MBR» : c типом EF

— для варианта «UEFI + GPT» : c типом EF00 и UUID = C12A7328-F81F-11D2-BA4B-00A0C93EC93B

Этот раздел должен быть отформатирован в FAT32 и иметь установленные флаги boot и esp. Размер раздела можно выбрать небольшим 200-300Мб (занято там будет, скорее всего, гораздо меньше, но делать его совсем маленьким, при современных объемах дисков — просто бессмысленно).

Если ESP раздел создается из установщика Ubuntu, то нужный формат, точку монтирования и необходимые флаги установщик сделает сам: нужно только выбрать тип раздела «EFI Boot» и задать его размер.

Если на компьютере уже стоит ОС загружающаяся через UEFI, то ESP раздел уже должен быть на диске и новый ESP создавать не нужно. В установщике Ubuntu надо только убедиться, что этот раздел используется как «EFI Boot». Более того, если создать несколько разделов ESP (на одном диске), то некоторые прошивки UEFI могут «окриветь» из-за такого неожиданного разнообразия. Общее правило — на диске должен быть только один раздел ESP. На разных устройствах ESP разделов может быть несколько — это вполне штатная ситуация для UEFI.

Для загрузки через UEFI модуль CSM не нужен, и, если все установленные на компьютере ОС грузятся через UEFI, то CSM лучше отключить. А вот если вы организуете загрузку в режиме SecureBoot, то CSM модуль отключить просто необходимо: без этого, обычно, просто не включить режим SecureBoot или включение SecureBoot автоматически запрещает работу CSM.

Менеджер загрузки UEFI

Кроме кода отвечающего за первичную инициализацию системы UEFI имеет свой менеджер загрузки. Работу менеджера загрузки задают переменные UEFI, хранящиеся в энергонезависимой памяти NVRAM 11) .

Переменные, которые задают работу менеджера загрузки это:

, где #### — шестнадцатеричный номер записи.

Переменные Driver#### инициализируют загрузку UEFI драйверов. Переменная DriverOrder определяет последовательность загрузки драйверов.

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

Переменные SysPrep#### инициализируют загрузку системных утилит. Переменная SysPrepOrder определяет последовательность утилит. Системных утилита должна выполнить свои действия и вернуть управление менеджеру загрузки UEFI. Утилиты не остаются в памяти, если нужен резидентный код, то его нужно загружать как Driver####.

Переменные Boot#### отвечают за загрузку загрузчиков ОС. Переменная BootOrder определяет приоритет загрузки загрузчиков ОС. Загрузчик обязан определить возможность загрузки ОС и принять решение — будет он загружать ОС или нет. Если ОС не будет загружаться, то загрузчик должен просто вернуть управление менеджеру загрузки UEFI. Если же ОС будет загружаться, то загрузчик должен уведомить об этом менеджера загрузки UEFI специальным вызовом. Этот вызов имеет принципиальное значение, т.к. те услуги которые UEFI обеспечивает для ОС гораздо уже и более ограничены чем те, которые доступны в процессе инициализации системы. И Именно для переключения режима услуг необходимо такое уведомление от загрузчика.

На работу менеджера загрузки влияют как переменные DriverOrder, SysPrepOrder и BootOrder, так и значение флага LOAD_OPTION_ACTIVE устанавливаемого в поле атрибутов загрузочной записи (Driver####, SysPrep#### или Boot####). Загрузочные записи с неактивным флагом LOAD_OPTION_ACTIVE игнорируются менеджером загрузки UEFI даже если эти записи указаны в переменной задающей порядок/приоритет (DriverOrder, SysPrepOrder или BootOrder). Также игнорируются записи с активным флагом LOAD_OPTION_ACTIVE, но не включенные в переменную задающую порядок/приоритет.

Менеджер загрузки UEFI в нормальном режиме работы сначала загружает все активные Driver#### упомянутые в DriverOrder и в том порядке, в котором они записаны в DriverOrder, затем загружает все активные SysPrep####, указанные в SysPrepOrder, в указанном там порядке, а после этого пытается загрузить активный загрузчик ОС (Boot####) в порядке указанном в BootOrder. А вот если ни одна из активных записей упомянутых BootOrder не смогла загрузить ОС (все вернули управление менеджеру загрузки UEFI), то запускается процесс восстановления.

Процесс восстановления настраивается переменными OsRecovery#### и PlatformRecovery####. Если эти переменные не заданы, то менеджер загрузки UEFI пытается произвести загрузку по умолчанию: загружается загрузчик хранящийся по пути \EFI\BOOT\BOOT.EFI, где — одно из:

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

Если заданы OsRecovery#### и/или PlatformRecovery####, то сначала выполняются OsRecovery#### и после этого менеджер загрузки UEFI пробует снова загрузить одну из Boot#### указанных в BootOrder и пробует загрузку по умолчанию.

Если и после этого ни один загрузчик не начал загрузку ОС, то выполняются PlatformRecovery####, но после выполнения записей PlatformRecovery#### загрузка начинается сначала: с Driver####, затем SysPrep####, а только после этого пробует снова загрузить одну из Boot#### указанных в BootOrder и загрузку по умолчанию. И вот если уже и после этого не удалось запустить загрузку ОС, то менеджер загрузки UEFI сдается и выводит сообщение о невозможности дальнейшей загрузки системы.

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

Еще один способ изменения приоритета загрузки ОС — выбор записи Boot#### по нажатию кнопки на клавиатуре в процессе загрузки. Для реализации такого механизма нужно привязать кнопку к записи Boot####. И если кнопка будет нажата, то менеджер загрузки пробует сначала загрузить запись Boot#### привязанную к кнопке, и только если загрузить ОС по этой записи не удалось, то менеджер загрузки переходит к загрузке в соответствии с BootOrder.

Практически все функции и настройки менеджера загрузки UEFI позволяет настроить утилита efibootmgr (смотрите man efibootmgr что бы узнать как).

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

Таким образом UEFI делает такие загрузчики (например GRUB) фактически ненужными. И ниже мы поговорим о том, как можно организовать загрузку ОС Linux из UEFI без дополнительных загрузчиков.

Secure Boot

Особого упоминания требует такая особенность UEFI как Secure Boot.

Как уже было сказано EFI раздел (ESP) находится на диске, к которому может быть организован доступ во время работы ОС. Это означает не только легкость в изменении загрузки и инициализации оборудования системы, но и то, что туда можно подсунуть заведомо зловредный код, который (О. УЖАС. ) будет загружен еще ДО!! загрузки ОС. На самом деле, перепрограммировать микросхемы BIOS или подменить код в MBR из запущенной ОС тоже можно было, но на это практически никто раньше не обращал внимания .

Закрыть эту «ужасающую дыру» в безопасности и призван Secure Boot. Вот как он работает:

При программировании системной памяти UEFI загрузчика (firmware), производители, вместе с кодом, записывают и сертификаты (содержащие ключи), и при загрузке в режиме Secure Boot UEFI проверяет по этим ключам подписи у любого исполняемого кода, который он загружает из ESP раздела (подписи добавляются к коду). Если верификация не прошла — такому коду отказывают в запуске.

Однако, довольно быстро шумиха улеглась. Во-первых Secure Boot практически все производители компьютеров разрешают отключать (Microsoft позже насколько переформулировал свои требования: от оборудования сертифицированного под Windows 8 не требуется запрещать загрузку без Secure Boot, зато требуется сообщить загрузчику Windows 8, что загрузка была не в SecureBoot режиме, и уже самой Windows решать — грузится ей дальше или нет). А во-вторых, в рамках консорциума UEFI были предусмотрены интерфейсы для управления ключами. «Подсуетились» и Canonical, и RedHat вместе с фондом FSF (Free Software Foundation). Как результат всех этих мероприятий и событий — и GRUB, и другие загрузчики обзавелись методами загрузки в режиме Secure Boot, а кроме того, стали доступны утилиты по управлению ключами UEFI. Вы даже можете свой собственный код подписать своим собственным ключом, загрузить ключ в UEFI (утилитами из пакета efitools) и это позволит загружать ваш собственный код в Secure Boot режиме.

Чуть хуже дело обстоит с планшетами и телефонами с предустановленной Windows — в прошивках этих устройств SecureBoot может быть не отключаем, а иногда нет и управления ключами.

Все дистрибутивы Ubuntu поддерживающие загрузку через UEFI уже имеют в своем составе все необходимое для загрузки в Secure Boot режиме практически на любом компьютере. Но, если загрузка с установочной флешки или диска не идет, то Secure Boot следует отключить (конечно же Secure Boot — не единственная причина по которой может не происходить загрузка с установочной флешки или диска).

Ключи системы Secure Boot

На самом деле в энергонезависимой памяти компьютера (NVRAM) рассчитанного на работу с UEFI предусмотрено место не на один сертификат, более того, сертификаты хранятся в специально организованной, иерархической системе. Давайте рассмотри этот вопрос более детально, но начнем с того, что определим — что такое сами ключи, о которых мы тут говорим.


Что же представляют собой эти ключи? На самом деле хранятся все ключи UEFI в сертификате — бинарном файле контейнере специального формата (специальное расширение стандарта x.509). Внутри сертификата могут содержаться открытый ключ, информация о владельце ключа, удостоверяющий ключ подписи и иная информация. Сами используемые ключи — это пары ключей (открытый/публичный и закрытый/секретный) сформированные алгоритмом RSA. UEFI стандарт рекомендует использовать ключи длинной 2048 bit.

Набор ключей системы SecureBoot

UEFI стандарт предусматривает следующие ключи (или списки ключей):

Кроме того в NVRAM UEFI могут быть созданы кастомные списки ключей. Примером такого списка служит список ключей MOK (Mashine Owner Keys). MOK ключи используются не самим UEFI, а загрузчиком Shim. Shim (подписанный ключем из db) загружается как обычное UEFI приложение и сам проверяет подпись загружаемого им кода по ключам из MOK.

Состояния системы SecureBoot

Работа с ключем PK сильно зависит от состояния в котором находится UEFI. В спецификации UEFI v2.5 предусмотрено 4 состояния, на два из них наиболее важны, а еще два — служебные (они могут быть вовсе не реализованы в некоторых прошивках UEFI). Состояния определяются значениями глобальных переменных UEFI (хранящимися в NVRAM). При этом, в разных состояниях меняется не только значение переменных, но и возможность записать в эти переменные новое значение. Далее будет использоваться следующая нотация: <Переменная>=<значение>/[RO|RW], где признаки RO и RW говорят о доступности переменной для изменения (RO, от ReadOnly — только чтение; RW, от ReadWrite — чтение и запись).

KEK ключ может быть добавлен в состояниях Setup (без подписи или само-подписанный) и User (обязательно подписанный секретным ключем от PK).

С ключами в db* (db, dbx, dbt, dbr) — все значительно проще и одновременно сложнее: Т.к. существует огромное количество операционных систем и их различных версий, а также загрузчиков этих ОС, драйверов и утилит от разных поставщиков, то поставить (от производителя) систему сразу со всеми нужными ключами — просто нереально. То же касается и ключей, которые были скомпрометированы и ключей восстановления. Поэтому предусмотрен режим добавления этих ключей. При этом если система находится в состоянии Setup, то ключи в db* можно добавлять без подписей или само-подписанные, а если в User, то добавляемые ключи должны быть подписаны приватной ключем от одного из KEK ключей или от PK.

Причем в ключи в db (dbt) могут добавляться и автоматически (если это предусмотрено конкретной реализацией UEFI): когда подпись загружаемого модуля не может быть проверена, то могут быть предусмотрены механизмы (поиск в массиве на диске или интерактивное подтверждение от авторизованного пользователя), которые разрешат добавить публичный ключ от подписи (подпись содержит публичный ключ для своей проверки) в db (или dbt).

Собственные ключи

Так как все ключи/сертификаты UEFI (PK, KEK, db) построены на основе открытых стандартов, то и весь набор этих ключей можно сформировать самостоятельно. Далее рассмотрим как это можно сделать, а вот тут описано все тоже другим толковым автором.

Для работы нам потребуются утилита openssh (практически во всех дистрибутивах Linux эта утилита уже установлена) и утилиты из пакета efitools (доступна в репозиориях ubuntu 14) ).

Создание ключей

Для начала создадим наш тестовый ключ и само-подписанный сертификат:

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

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

Сначала мы создаем контейнер EFI_SIGNATURE_LIST содержащий сертификат:

Далее создадим подписанные ключи для последующей загрузки в энергонезависимую память UEFI. Наши файлы будут содержать сертификат с префиксом в виде EFI_VARIABLE_AUTHENTICATION_2 описателя. Описатель будет подписывать ключ, и содержать название переменной и другие атрибуты. Т.к. в файл будет включено название переменной (PK, KEK and db), нам придется создать отдельный контейнер для каждой переменной.

Кроме того, все известные сертификаты от разработчиков linux (Canonical, Fedora, AltLinux, openSUSE и др.) — тоже само-подписанные, их вы можете найти в на сайте проекта rEFInd или в проекте UEFI-Boot на github

Создание хранилища ключей

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

На самом деле вы можете создать хранилище где угодно и указать утилите sbkeysync где оно находится в значении ключа –keystore.

Загрузка ключей
Через утилиту прошивки

Загрузка ключей в UEFI может быть выполнена из утилиты настройки вашей материнской платы. Там нужно будет найти раздел (обычно Security) где задаются параметры SecureBoot. Возможно потребуется явно разрешить работу с ключами — выбрать режим управления ключами custom или как либо еще. Попав в окно утилиты по управлению ключами, первым делом сохраните (на всякий случай) фабричные ключи 15) . После этого удалите все фабричные ключи, и по одному добавляйте ключи из вашего хранилища ключей (скорее всего, его придется перенести на ESP раздел, т.к. только этот раздел доступен для UEFI).

PK ключ желательно записывать последним (почему? — детально описано в финальной части раздела "Ключи системы SecureBoot").

Следует также отметить, что формат, в котором прошивка умеет загружать ключи может разниться в разных прошивках UEFI. Например прошивка AMI из недавно купленного мини-PC может взять сертификат и из подготовленного контейнера, и непосредственно из файла .pem. Т.о. вся эта возня со специальными контейнерами, в этом случае, вовсе не нужна: один и тот же само-подписанный сертификат можно загрузить и в PK, и в KEK, и в db.

Используя sbkeysync

Если UEFI находится в Setup режиме, то ключи можно загрузить и из ОС, используя утилиту sbkeysync. Но для начала воспользуйтесь опцией –dry-run, что бы убедится, что у вас все верно настроено и готово к этому важному процессу:

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

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

Подписывание загрузчика

Т.к. мы активировали SecureBoot, то теперь нам нужно подписать наш загрузчик что бы он мог быть загружен UEFI в этом режиме. В нашем примере мы подпишем код загрузчика GRUB2.

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

Возврат в режим Setup

Хотя лучше убедиться, что вернуть UEFI в режим Setup можно из самой прошивки (утилиты настройки) перед тем как прописывать PK.

Как Ubuntu загружается в режиме Secure Boot

Как уже было сказано выше, сертификаты с ключами для загрузки в режиме Secure Boot записываются производителем оборудования (довольно подробно о ключах UEFI (англ.)). И, как правило, набор сертификатов состоит из: сертификата производителя, KEK и db ключей от Microsoft (конечно же). Крайне редко, но все же попадаются такие машины, у которых в db есть и ключ от Canonical и/или RedHad. Как же на машине c ключами только от Microsoft загрузить Ubuntu в режиме SecureBoot?
Решение было найдено: Microsoft согласился (не без активности со стороны Canonical) подписать своим ключом простенький загрузчик от Red Hat — shim (есть еще мини-загрузчик — PRELoader, о его подписании ключом Microsoft похлопотал фонд FSF). Shim, в свою очередь, содержит (в MOK) UEFI сертификат от Canonical и проверяет подпись GRUB2 (версия которого, распространяемая через репозитории Ubuntu, подписана ключем Canonical). А GRUB2 проверяет подпись ядра (из пакета linux-signed-generic, которое подписано Canonical).

Введение дополнительного звена в виде shim продиктовано тем, что GRUB довольно часто обновляется и каждый раз после обновления его надо подписывать. Само собой, его гораздо проще подписать собственным ключом Canonical нежели каждый раз снова договариваться с Microsoft.

Если вы не хотите оставлять сертификаты Microsoft и производителя на своей машине 16) , то из утилиты настройки вашей материнской платы или с помощью утилит efitools можно стереть сертификаты прописанные производителем, и записать в UEFI (db) ключ от Canonical, тогда уже Grub (так же как и само ядро Linux) сможет загружаться в режиме Secure Boot, и shim (со своим MOK) — не нужен.

Если вам не хочется даже сертификат от Canonical оставлять в своем компьютере 17) , то вы можете создать свои собственные ключи и сертификаты (как описано выше), подписать grub или само ядро Linux (утилитой sbsign из пакета sbsigntool) и записать ключи в базу данных ключей EFI (утилитами efitools). Подробно этот процесс описан выше и тут (англ.).

ВНИМАНИЕ, если ваш компьютер не позволяет отключить Secure Boot режим, то все манипуляции с ключами и подписями нужно проделывать с предельной осторожностью, т.к. любая ошибка может привести к тому, что ваш компьютер превратится в «кирпич».

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

Хорошая идея: проделать все сначала на виртуальной машине.

EFS раздел (необходимый для загрузки в UEFI режиме) прописан и в таблицу разделов в MBR, и в каталог разделов iso9660 формата. По UEFI стандарту загрузчик по умолчанию должен находится в EFS разделе по пути: EFI\BOOT\BOOTx64.EFI

Такой «винегрет» позволяет грузиться с такого образа в следующих режимах:

Кстати в EFI\BOOT\BOOTx64.EFI (EFI\BOOT\BOOTia32.EFI для 32-х битных платформ) лежит не сам GRUB, а SHIM. Сам grubx64.efi/grubia32.efi (начальная стадия grub-efi) лежит рядом (в EFI\BOOT\) и его запускает SHIM. SHIM имеет валидную подпись ключом от Microsoft для загрузки в режиме SecureBoot, что позволяет загрузиться из образа на большинстве компьютеров.

Такой образ легко записать на флешку простой командой:

Второй параметр — именно девайс, а не раздел. Посмотреть диски и разделы можно в выводе команды

Образ Ubuntu копируется начиная с первого сектора устройства, MBR и таблица разделов ISO9660 стандарта попадают в необходимые для их правильной работы места, что позволяет с загрузиться с такой флешки как c USB-СD/DVD и как с USB-HDD/USB-Flash. Причем в обоих вариантах доступна загрузка как в UEFI, так и в BIOS/CSM режимах.

Другие интересные возможности UEFI

UEFI Shell

UEFI shell часто уже установлен в разделе ESP или прямо в прошивке UEFI. Некоторые UEFI прошивки умеют загружать командный интерпретатор UEFI прямо из консоли настройки UEFI (BIOS).

Если вы ставили Ubuntu на чистый диск, а в прошивке нет встроенного UEFI shell, то его можно доставить руками. Сам бинарный файл легко находится через любой поисковик. Вам нужно будет только скопировать его в корень ESP раздела с именем shellx64.efi (для 32-х битных платформ: shellx32.efi). Само собой копировать надо через sudo (т.е. с правами root).

Если из настроек UEFI нет возможности запустить UEFI shell, то можно его прописать как вариант загрузки (как вы помните UEFI поддерживает мульти-загрузку). Для этого воспользуемся утилитой efibootmg:

После этой команды у вас появится новый пункт меню загрузки UEFI, и это новый пункт станет первым по приоритету. Если такой приоритет загрузки вас не устраивает, то поменяйте его с помощью опции «-o» утилиты efibootmgr.

Справочник по командам UEFI shell встроенный — команда help. Если вы хотите останавливать вывод постранично (некий аналог more) то используйте в командах ключ -b. Кроме того поддерживается история вывода на 3 страницы которые можно просмотреть используя кнопки PageUp и PageDown.

Команда map покажет известные UEFI диски и разделы. Обычно диск fs0: — это и есть раздел ESP. Для того, что бы просмотреть содержимое каталогов ESP раздела (ls) выполните переход на ESP раздел — введите в строке приглашения fs0: — это очень похоже на dos команды типа а: с: (если кто еще помнит такое).

Разделитель в путях тоже в стиле DOS — обратный слеш (\).

EFI shell имеет довольно обширный набор команд, все их тут описывать смысла нет — если не достаточно встроенной подсказки, то есть краткие руководства в Internet. Наиболее полное писание UEFI Shell и его команд я нашел только в спецификации UEFI Shell 2.0 на сайте

Загрузка ядра Linux непосредственно из UEFI

Если вы взгляните в файловом менеджере на ядро (/boot/vmlinuz*) то вы можете немало удивиться, заметив что тип будет указан как «DOS/Windows executable» — не удивляйтесь. Все последние ядра в Ubuntu собираются с опцией UEFISTUB (поддержка загрузки в режиме UEFI). А это добавляет заголовок аналогичный ДОСовскому .exe (именно так должны собираться все UEFI приложения). Т.е. само ядро можно загрузить как UEFI приложение. Для этого нужно разобраться с двумя основными вопросами:

1. Как правильно передать ядру параметры? Т.е. указать на корневую файловую систему, образ initrd, и указать опции загрузки ядра.
2. Как обеспечить UEFI доступ к самому ядру (и initrd)?

По первому пункту все достаточно просто: параметры, которые нужно передать ядру для загрузки можно подсмотреть в /boot/grub/grub.cfg, в команде загрузки linux (там стандартно передаются указание на корневой раздел и опции типа «ro», «quiet» и «splash»). Дополнительно ядру надо будет сообщить, где найти initrd (в ядрах версии 3.3 и выше это можно сделать через параметр intrd). В итоге, та команда, которую должен выполнить UEFI будет выглядеть примерно так:

В этом примере ядро и initrd лежат непосредственно в корне ESP раздела. Ядро переименовано в «vmlinuz.efi», а соответствующий ему образ рамдиска начальной инициализации ядра в «initrd». В команде указан UUID (моего корня, вам нужно прописать свой UUID) как путь к корневому разделу, но можно написать и что-то типа root=/dev/sda2 (UUID все же — лучше).

В первом варианте — для загрузки из UEFI нужно использовать подписанный вариант ядра (vmlinuz*.efi.signed).
Во втором варианте нужно организовать (это можно даже автоматизировать) подписывание каждого нового ядра, которое приходит с обновлениями системы.

Некоторые сложности может вызвать указание пути к initrd. Ядра выше 3.3 вообще не различают прямые и обратные слеши в параметре initrd, но нужно указать такой путь, что бы ядро смогло найти initrd при загрузки в окружение UEFI.

В целом, нам необходимо организовать доступ из UEFI и к ядру, и к initrd (напомню: UEFI имеет доступ только к ESP разделу и умеет работать только с файловой системой FAT32). Это может быть решено одним из трех вариантов:

Вариант 1: Загрузить ядро и intrd непосредственно из корневого раздела или раздела boot

Для этого нужно загрузить в UEFI драйвер для поддержки той файловой системы, в которую отформатирован ваш корень или /boot. (UEFI «из коробки» знает только FAT32, драйвера для чтения других FS можно найти например тут).

Вот какие команды надо выполнить в UEFI Shell для загрузки ядра прямо из корня, находящегося на разделе с EXT4 (boot не вынесен в отдельный раздел).
— загрузить драйвер поддержки ext2-3-4 (в нашем примере он был предварительно размещен на ESP разделе в каталоге EFI/drivers)

— смонтировать корневую FS (эта команда пытается все доступные устройства смапить всеми доступными драйверами)

— перейти (изменить текущий путь) в новую FS

— запустить ядро с параметрами

В этом примере я воспользовался тем, что в корне корневой FS автоматически создаются линки на самую свежую версию установленного ядра и его initrd. Перейдя (сменив текущий путь) в корневую ФС мне уже не нужно мудрить с указанием полного пути к ядру и initrd — они находятся в текущем каталоге (из которого и запускается ядро).

Все эти действия можно записать в скрипт (для скриптов UEFI Shell принято расширение .nsh), и запускать ОС вызвав его. К сожалению, непосредственно скрипт файл нельзя указать как исполняемый файл в пункте меню загрузки UEFI. Согласно спецификации UEFI Shell может исполнить скрипт, имя которого передано ему как параметр, но мне это сделать не удалось. UEFI Shell может автоматом исполнить (после паузы) скрипт startup.nsh, находящийся в корне ESP раздела (можно записать эту последовательность команд туда).

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

Дело в том, что просто загрузить драйвер — недостаточно, нужно еще вызвать процедуру ремаппинга устройств (т.е. выполнить ту самую команду map -r). При ремапинге каждый драйвер пытается «прицепиться» ко всем доступным устройствам, т.е. наш драйвер EXT2-4 сделает доступными все разделы на всех дисках с файловыми системами EXT2-4.

Добавить автоматическую загрузку драйвера можно командой

То же самое можно сделать командой bcfg из UEFI-Shell. Следующая команда добавит пункт загрузки Driver0000 (номер задает первый аргумент команды add), который будет загружать драйвер из файла EFI\drivers\ext2_x64.efi (с ESP раздела). Драйвер получит описание «EXT2-4 Driver»:

Все замечательно и этот драйвер, при каждом запуске системы, будет загружаться во время инициализации UEFI автоматически, но ремапинга не будет. Для того, чтобы после загрузки драйвера инициировать ремапинг нужно в опции загрузки драйвера указать специальный атрибут LOAD_OPTION_FORCE_RECONNECT, однако ни одной командой UEFI-Shell или опцией efibootmgr этот атрибут не установить (по крайней мере мне не удалось найти такой команды). Перелопатив спецификацию UEFI можно понять, что нужно то всего прописать значение 3 вместо 1 в первом 32-битном слове UEFI переменной отвечающей за загрузку драйвера (той самой Driver0000, что мы создали командой bcfg или efibootmgr). Сделать это изменение можно любым HEX редактором (из загруженной системы), запущенным с правами рута, в котором на редактирование открывается файл /sys/firmware/efi/efivars/Driver0000-8be4df61-93ca-11d2-aa0d-00e098032b8c (это маппинг в файловую систему sys переменной UEFI). В редакторе нужно 5-й байт от начала поменять с значения 01 на 03 и сохранить файл.

Хорошим тоном будет вернуть защиту после редактирования командой chattr +i

После нашей хакерской атаки на UEFI можно перегрузиться в UEFI-Shell и там прямо при запуске увидеть, что все EXT4 разделы уже отмаплены в FS<n> «диски», а значит на них можно сослаться при задании команды загрузки для опции загрузки ОС. В моем примере EXT4 раздел с корневой FS отмапился в FS2. А это позволяет задать в опции загрузки (используя команду bcfg из UEFI-Shell или efibootmgr из загруженной системы) команду такого вида:

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

В принципе, несмотря на некоторую сложность этого метода в реализации, это САМЫЙ ПРАВИЛЬНЫЙ вариант загрузки ядра Linux из UEFI. Дополнительные удобства создают постоянно обновляемые ссылки на последнее и предыдущее ядра и их initrd в корне файловой системы Linux. На них можно однократно создать пункты загрузки UEFI и не нужно никаких обновлений после установки/удаления ядер и обновления initrd. Также однократно надо скопировать в EFS раздел драйвер для ФС корня и однократно настроить его загрузку.

Вариант 2: Скопировать ядро и intrd в ESP раздел

Преимущество в том, что не нужно мудрить с организацией поддержки другой FS. Однако, каждый раз при обновлении ядра или initrd их нужно вновь копировать в ESP раздел. Решить задачу автоматизации этого процесса можно одним из методов описанных в этой статье (англ.) или подобно тому как это реализовано в проекте UEFI Boot.

Вы можете выполнить команду запуска ядра из UEFI Shell, или записать ее в командный файл и запускать его из UEFI Shell, или, воспользовавшись утилитой efibootmgr, записать эту команду как пункт меню загрузки UEFI:

Эта команда добавит пункт загрузки LinuxKernel и сделает его первым в списке приоритета загрузки.

Вариант 3: Смонтировать ESP раздел как /boot

Преимущества этого решения в том, что новые ядра и initrd будут находится (устанавливаться и обновляться) прямо на ESP раздел (который станет одновременно boot разделом). Правда, неприятность в том, что название файла с ядром и initrd есть версия, и для каждого нового ядра нужно заново прописывать команду в пункт загрузки UEFI. Можно, конечно, переименовать в короткие имена (которые однократно будут записаны в пункты загрузки UEFI), однако переименование приводит нас к ситуации близкой к предыдущему случаю — initrd будет при обновлении снова получать номер версии и его каждый раз придется переименовывать. Воспользоваться автоматически создаваемыми ссылками в корневом разделе — не получится (они останутся в файловой сиcтеме корневого раздела, к которому нет доступа без доп. драйверов), а создать ссылки на FAT разделе — невозможно (этого FAT просто не умеет от рождения).

Команда для записи пункта загрузки UEFI через утилиту efibootmgr не отличается принципиально от той, что указана для способа с копированием ядра в ESP раздел:

Более детальная проработка этого варианта описана в отдельной статье UEFI Boot. Там реализовано автоматическое обновление пунктов загрузки UEFI при установке, обновлении и удалении ядер.

Устранение из процесса загрузки загрузчиков (оригинально это цепочка из двух: shim + GRUB) заметно (но не так что бы очень значительно) сокращает время загрузки ОС. Вот как это выглядит в цифрах на примере одного mini-pc (I5 5257U, 8Gb RAM, SSD): утилита systemd-analyze сообщает, что на стадию работы загрузчиков в случае цепочки UEFI-Shim-GRUB-Kernel требуется чуть меньше секунды — 967ms, а на прямую загрузку UEFI-Kernel — 153ms. При полной (холодной) загрузке системы за

14 секунд выигрыш составляет порядка 6%.

Полезные утилиты для UEFI

В стандартных репозиториях Ubuntu есть несколько полезных пакетов с утилитами для работы с настройками UEFI.

У всех этих утилит есть вполне толковые man-руководства и есть примеры использованию в Интернете, поэтому я не стану останавливаться на деталях использования этих утилит.

🙂 Патч Бармина живе всех живых

Ну и напоследок, последняя «веселая» история связанная с бородатым (по некоторым сведениям 1996 года рождения) патчем Бармина.

В конце января 2016 некий арчевод решил посмотреть — как работает этот известный патч. И он старательно вписал в команду даже специальный ключ, без которого этот патч уже не запускается… Ну… и получил кирпич из своего MCI нетбука — он даже после сброса не включился!

Как нетрудно догадаться корень беды — в кривой прошивке UEFI. Патч вытер вместе с корнем еще и переменные UEFI в NVRAM, которые монтируются в /sys/firmware/efi/efivars/, но принципиально это не могло быть проблемой, потому как стандартом UEFI предусматривается нарушение целостности данных в NVRAM: при обнаружении такой ситуации прошивка ОБЯЗАНА осуществить инициализацию NVRAM до состояния настроек по умолчанию/фабричных (Factory Default). Но вот в MCI решили забить на проверку целостности NVRAM, и незадачливый арчевод потащил свой нетбук кирпич в сервис.

После этой новости состоялся наезд на разработчиков SystemD (ну на них многие любят наезжать и ругаться, а те собственно сами довольно часто дают повод для вполне обоснованной и справедливой ругани в свой адрес): мол какого лешего, SystemD монтирует эти переменные с возможностью записи!? Давайте типа быстро переделайте на монтирование в режиме только-чтение. На что был дан резонный ответ — доступ на запись нужен утилитам, и разрешена запись только руту, который при желании премонтирует эти переменные в режиме записи. Так что, это не защитит от идиотов дураков «умников», которые экспериментируют с патчем Бармина.

Правда позже некоторые контрмеры все-таки были предприняты: теперь все UEFI переменные монтируются со специальным атрибутом, который запрещает запись и удаление этих файлов даже руту. Однако root в состоянии снять эти флаги (сhattr -i <имя файла>). Так что теперь для повторения судьбы арчевода с ноутбуком MCI нужно не только специальный ключ в команде rm -rf задавать, но еще и предварительно снять защиту с перемнных UEFI.

Самое же примечательное в этой ситуации ИМХО в том, что 20 лет назад отпущенная шутка, до сих пор стреляет, да еще с невиданной доселе мощью.

Раскрываем секреты загрузочных ISO-образов

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

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

Также у меня была статья на Хабре, где я затрагивал тему загрузочных ISO, но не вдавался в подробности реализации.

Что вы найдёте в этой статье

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

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

После прочтения этой статьи вы сможете создать универсальный загрузочный образ ISO, который можно будет записать на оптический диск, внешний жёсткий диск или флеш-накопитель так, что при загрузке с него будут выполняться минимальные программы Hello, World, никак не зависимые от операционной системы. Эти программы можно считать заглушками для реальных загрузчиков операционных систем. Пример из статьи позволит вам разобраться со структурой загрузочных ISO-образов и улучшит ваше понимание процесса загрузки компьютера (bootstrapping).

Немного теории, которую необходимо знать

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

Загрузка операционной системы на компьютере x86

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

Существует два вида программ, располагающихся в ROM — BIOS (Basic Input/Output system) и UEFI (Extensible Firmware Interface). Обычно компьютеры с UEFI позволяют эмулировать BIOS, если выбрать опцию Legacy Mode.

Механизм загрузки BIOS (Legacy)

BIOS не имеет никакого представления о файловых системах, установленных на устройствах, с которых осуществляется загрузка. Он рассматривает устройства как блочные, которые могут возвращать блоки данных (сектора), располагающиеся по определённым LBA (Logical Block Address). Ранее дисковые устройства ещё имели режим адресации CHS (Cylinder, Head, Sector), который был более близок к физической геометрии жёстких дисков, но сейчас он практически не используется.

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

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

Когда BIOS передаёт управление, то процессор x86 находится в реальном режиме, а это значит, что в программе можно обратиться только к 1МiB оперативной памяти и, если вы хотите использовать всю оперативную память, то необходимо позаботиться о том, чтобы перевести процессор в защищённый режим. Такие загрузчики, как GRUB 2, как раз это и делают.

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

UEFI знает о файловой системе FAT. При загрузке с устройства UEFI пытается найти на устройстве раздел с этой файловой системой и исполняемый файл UEFI в ней, после чего запустить его на выполнение.

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

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

Загрузка с жёсткого диска или флеш-накопителя

Загрузка с жёсткого диска или флеш-накопителя в большинстве случаев подразумевает, что они размечены на разделы, а на одном или нескольких разделах находятся загрузчики и операционные системы, которые необходимо загрузить. Мне известно о двух видах разметки: MBR (Master Boot Record) и GPT (GUID Partition Table).

MBR – более ранний способ разметки, подразумевающий разметку диска на 4 первичных раздела. Существует способ увеличить общее количество разделов за счёт создания так называемых расширенных разделов, но загрузка с таких разделов усложняется.

GPT – современный способ разметки, подразумевающий разметку диска на большое количество разделов и поддержку дисков размером более 2TiB.

На просторах Интернета существует очень распространённое заблуждение, которое заключается в том, что на компьютерах с BIOS используется ТОЛЬКО MBR-разметка, а с UEFI TOЛЬКО GPT-разметка. Это неверно.

BIOS не имеет понятия как о GPT, так и о MBR, он только знает о том, что нужно загрузить и выполнить программу, находящуюся в нулевом секторе, если этот диск является загрузочным и последние два байта в секторе равны 55AAh. Но если в результате будет передано управление загрузчику, который «понимает» GPT, то последний сможет загрузить операционную систему с раздела GPT. Разумеется, сама операционная система должна поддерживать GPT-разметку. Но даже если она не поддерживает GPT, в нулевом секторе можно разместить таблицу разделов MBR, которая будет указывать на подмножество разделов GPT (максимум 4 шт., которые находятся в пределах первых 2ТiB размеченного диска), и они будут видны этой операционной системе.

Чтобы компьютер загрузил ОС средствами UEFI, необходимо наличие системного раздела EFI на диске (EFI System Partition (ESP)), с которого осуществляется загрузка. Неважно, какая разметка используется (MBR или GPT), важно наличие самого раздела, а также наличие приложения UEFI с прописанным именем в системных переменных UEFI или имеющего стандартное имя bootx64.efi и располагающегося в директории EFI\boot в разделе ESP.

Таким образом, различие BIOS и UEFI-загрузки заключается в том, как загрузится и выполнится самая первая программа (приложение) после них. В BIOS загружается и выполняется программа, которая находится в нулевом секторе диска, в UEFI выполняется программа, которая находится на системном UEFI разделе диска.

Файловая система оптических дисков

Для хранения файлов на оптических дисках очень часто используется файловая система ISO 9660. Она имеет несколько уровней (Level 1, Level 2, Level 3) и расширений (Rock Ridge, Joliet). Также часто используется файловая система UDF, но для нашей задачи эти знания не понадобятся, так как фактически мы будем работать с секторами диска. В статье я не буду подробно рассматривать файловые системы для оптических дисков. Загрузчик, который будет загружен BIOS или UEFI для выполнения дальнейшей загрузки, должен «понимать» файловую систему оптических дисков, а я для краткости это не рассматриваю.

Загрузка с оптического диска

Для того, чтобы компьютер смог загружаться с оптического диска, BIOS или UEFI должны поддерживать стандарт El Torito, придуманный специально для этого. И тут, как и при загрузке с жёсткого диска, для BIOS важен сектор, в котором находится программа начальной загрузки, а для UEFI важен системный UEFI-раздел на диске c программой UEFI.

Скажу честно, я до последнего думал, что UEFI запускает файл bootx64.efi, который находится в файловой системе ISO 9660 на оптическом диске, однако это не так, так как UEFI понимает загрузку только с ESP, поэтому ему нужно предоставить образ ESP с файлом bootx64.efi, а в специальной структуре Booting Catalog указать расположение образа.

Гибридный ISO-образ

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

Суть гибридных ISO-образов в следующем: один и тот же образ можно посекторно записать на оптический диск или на флеш-накопитель/внешний жёсткий диск. Загрузка будет выполнена с любого из устройств, если его выбрать в качестве загрузочного. Это очень удобно для конечного пользователя. Он просто берёт образ и записывает его на оптический диск или флеш-накопитель, не выполняя никаких преобразований.

Как же это происходит? Дело в том, что первые 16 секторов в файловой системе ISO 9660, каждый из которых размером 2048 байт, используются для системных нужд, поэтому там можно разместить MBR и/или GPT. Они игнорируются BIOS/UEFI при загрузке с оптического диска, но при загрузке с флеш-накопителя они используются.

Запись о разделе в MBR или GPT будет указывать на файл с образом ESP, а в случае MBR для BIOS будет содержаться код начальной загрузки, который уже будет каким-то образом читать код из секторов и выполнять его, продолжая тем самым дальнейшую загрузку.

При всём своём удобстве этот подход не стандартизован и, например, Windows или какие-то программы по разметке диска могут считать такую информацию в MBR или GPT неверной.

Структура загрузочного ISO-образа

Любой ISO-образ должен содержать список дескрипторов томов. Каждый дескриптор занимает целый сектор (2048) байта. Дескрипторы томов размещаются начиная с 16-го сектора, если нумерация секторов осуществляется с 0.

Дескриптор тома с загрузочными записями El Torito располагается в 17-м секторе.
Структура загрузочного ISO-образа для современных компьютеров х86 представлена на рисунке.

Для загрузки с оптического диска нас интересует Boot Record Volume Descriptor. С описанием Primary Volume Descriptor и Volume Descriptor Set Terminator вы можете ознакомиться в спецификации файловой системы ISO 9660 (ECMA-119)

▍ Boot Record Volume Descriptor
Байты Описание
0 Индикатор загрузочной записи, всегда содержит значение 0x00
1… 5 Идентификатор ISO-9660, всегда содержит строку CD001
6 Версия дескриптора, содержит 0x01
7… 38 Идентификатор загрузочной системы, всегда содержит строку EL TORITO SPECIFICATION, оставшиеся байты заполняются 0
39… 70 Не используется. Всегда заполнено 0
71… 74 LBA первого сектора загрузочного каталога
75… 2047 Не используется, всегда заполнено 0
▍ Booting Catalog

Booting Catalog – это массив записей размером 32 байта каждая. Количество записей не ограничено. Всего в спецификации определены 5 типов записей: Validation Entry, Default Entry, Section Header, Section Entry, Section Entry Extension. Последнюю описывать не буду, так как я не встречал её применение.

Validation Entry

Назначение Validation Entry — это подтверждение того, что образ ISO (оптический диск) действительно является загрузочным.

Байты Описание
0 Header Id всегда равен 0x01
1 ID платформы
0x00 = 80×86
0x01 = Power PC
0x02 = Mac
2..3 Зарезервировано, всегда содержит 0
4..27 ID, идентификатор производителя CD-ROM, сейчас обычно содержит 0
28..29 Контрольная сумма
30 Всегда 0x55, участвует в подсчёте контрольной суммы
31 Всегда 0xAA, участвует в подсчёте контрольной суммы

Default Entry

Назначение Default Entry — указать, что будет загружено по умолчанию. Обычно это сектора для загрузки при помощи BIOS.

Байты Описание
0 Boot Indicator (0x88 — загрузочная, 0x00 — не загрузочная)
1 Boot media type
0x00 — без эмуляции
0x01 — эмуляция дискеты 1.2
0x02 — эмуляция дискеты 1.44
0x03 — эмуляция дискеты 2.88
0x04 — эмуляция жёсткого диска
В настоящее время чаще всего используется режим 0x00. Эмуляция дискет и жёстких дисков нужна для загрузки очень старых операционных систем.
2 Сегмент загрузки обычно 0x00, что подразумевает, что сектор с кодом загрузчика будет помещён в сегмент 0x07C0, как и в загрузке с жёсткого диска или дискеты
4 Тип системы для режима без эмуляции – 0x00
5 Не используется
6..7 Количество секторов размером 512 байт (не 2048), которые нужно считать в сегмент загрузки
8..11 LBA-адрес первого сектора, который нужно загрузить в сегмент загрузки
12..31 Не используются, всегда содержат 0

Section Header

Назначение Section Header — сгруппировать возможные виды загрузки, однако обычно на практике существует ещё только один вид загрузки через UEFI.

Байты Описание
0 Header Indicator (0x90 – не последний заголовок, 0x91 – последний заголовок). В большинстве случаев, так как обычно в ISO-образе только одна секция, поле содержит 0x91.
1 Идентификатор платформы.
0x00 — 80×86
0x01 — Power PC
0x02 — Mac
Значение 0xEF отсутствует в оригинальной спецификации, но сейчас оно используется, если мы хотим выполнить загрузку средствами UEFI.
2..3 Количество Section Entry после заголовка.
4..31 Идентификатор для BIOS и загрузчика, чтобы он принял решение, использовать ли информацию из следующих секций для загрузки. В большинстве случаев тут содержатся 0.

Section Entry

Назначение Section Entry — указать, что может быть загружено и откуда. Обычно это сектора образа ESP для загрузки при помощи UEFI.

Байты Описание
0 Booting Indicator (0x88 — загрузочная, 0x00 — не загрузочная)
1 Режим эмуляции (Boot media type)
0x00 — без эмуляции
0x01 — эмуляция дискеты 1.2
0x02 — эмуляция дискеты 1.44
0x03 — эмуляция дискеты 2.88
0x04 — эмуляция жёсткого диска
В настоящее время чаще всего используется режим 0x00. Эмуляция дискет и жёстких дисков нужна для загрузки очень старых операционных систем. В оригинальной спецификации вы можете посмотреть, какие ещё значения может содержать этот байт.
2..3 Сегмент загрузки обычно 0x00, что подразумевает, что сектор с кодом загрузчика будет помещён в сегмент 0x07C0, как и в загрузке с жёсткого диска или дискеты
4 Тип системы для режима без эмуляции – 0x00
5 Не используется, всегда равен 0x00
6..7 Количество секторов размером 512 байт (не 2048), которые нужно считать в сегмент загрузки
8..11 LBA-адрес первого сектора, который нужно загрузить в сегмент загрузки
12 Selection Criteria Type. Критерий выбора. Обычно 0. Подробнее смотрите в спецификации El Torito
13..31 Vendor Unique Selection Criteria. Обычно 0x00. Подробнее в спецификации El Torito

Практическая часть

Создание образа

Изначально я хотел описать в статье, как создать образ с нуля без использования сторонних программ, но потом мне показалось, что это раздует статью, её будет сложно читать, и вы потратите много времени на изучение. Поэтому использовал уже готовые решения, которые используются в Linux: Xorriso для создания образов, а для того, чтобы увидеть наглядно структуры загрузочного ISO, xxd для просмотра содержимого образа в шестандцатиричном формате

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

Минимальные программы-заглушки загрузчиков для BIOS и UEFI

Загрузчик для BIOS — это один или несколько секторов на диске, содержащих инструкции, которые используют сервисы (программные прерывания) BIOS и осуществляют дальнейшую инициализацию компьютера, загружают и запускают на выполнение ядро операционной системы.

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

Для упрощения понимания наши загрузчики не будут ничего делать кроме вывода тривиальных фраз типа «Hello, world!» на экран. Можно сказать, что это заглушки, которые могут быть использованы для написания полноценных загрузчиков в дальнейшем.

Код программы-заглушки загрузчика BIOS

Загрузчик для BIOS – это один или несколько секторов на диске. Инструкции, содержащиеся в них, процессор выполняет в реальном режиме, где доступно только 1MiB оперативной памяти.
Обычно исходный код загрузчика при помощи ассемблера превращается в бинарный файл, содержимое которого потом посекторно записывается на диск.

Для меня проще всего получить такой код, используя ассемблер fasm.

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

  1. Так как процессор будет выполнять код в реальном режиме, нам нужно использовать директиву use16.
  2. Наш код будет загружаться в BIOS по адресу 0x7c00. Чтобы ассемблер закрепил корректные адреса за метками, используется директива org.
  3. Чтобы аппаратные прерывания не помешали начальной инициализации регистров, используется команда процессора cli.
  4. Очень важно проинициализировать регистры. Во многих статьях по созданию загрузочных секторов забывают этот факт. В результате вроде всё гладко работает в qemu или VirtualBox, однако в VMWare или на реальном железе код отказывается работать. Я потратил достаточно много времени, пока это не понял.
  5. Так как часть полученного кода будет модифицироваться Xorriso, который записывает нужные данные, то нам необходимо выполнить команду jmp, а дальше в коде зарезервировать место под эти данные.
  6. Я не создавал свою MBR для ISO-образа, а использовал ту, которую предоставляет загрузчик ISOLINUX, поэтому необходимо прописать значение signature, ожидаемое загрузчиком из MBR, иначе наша заглушка загрузчика для BIOS не будет выполнена.
  7. Значение 55AA и заполнение нулями в конце программы необязательно, но я хотел получить полноценный загрузочный сектор, который можно было бы записать в нулевой сектор жёсткого диска или флеш-накопителя, и он тоже бы выполнялся.

Код программы-заглушки загрузчика UEFI

Загрузчик для UEFI – это уже исполняемый файл, который помимо программного кода и данных содержит дополнительную информацию о том, как размещать их в оперативной памяти. Файлы .efi по формату практически совпадают с исполняемыми файлами Windows. Разумеется, в них нет вызовов функций операционной системы. Обычно исходный код пишется на языке C или С++. Мы для простоты будем использовать C.

При сборке в операционной системе Linux нам понадобится кросс-компилятор.

Необходимости комментировать что-то в программе нет ввиду её простоты, однако я добавлю, что для компиляции программы у вас должен быть установлен пакет gnu-efi, а вечный цикл (while (1)) я сделал для наглядности, чтобы сообщение не исчезало при завершении работы программы, и EFI_SUCCESS она никогда не вернёт.

Алгоритм создания загрузочного диска вручную

Я собирал образ загрузочного диска на Ubuntu 22.10.

Для начала необходимо поставить следующие пакеты, если они не установлены.

Пакет Назначение
make Утилита make
gcc-mingw-w64 Компилятор C
fasm Ассемблер
gnu-efi Библиотека для создания UEFI-приложений
binutils Утилиты для работы с объектными файлами
mtools Утилиты для работы с образом раздела FAT
dosfstools Утилиты для форматирования
xorriso Утилита для создания ISO-образов
isolinux Загрузчик, который позволяет загружаться с оптических дисков. Нам от него нужен только MBR для гибридного ISO
xxd Утилита для просмотра бинарных файлов. В частности, она нам понадобится для просмотра структуры созданного ISO-образа
qemu-system-x86 Эмулятор, который позволяет эмулировать компьютеры с BIOS и UEFI
ovmf Образ прошивки UEFI для эмулятора

1. Создаём необходимые директории.

2. Создаём файлы bioshello.asm и efihello.c с исходным кодом наших программ-заглушек загрузчиков.

3. Компилируем заглушку для загрузчика BIOS.

4. Компилируем заглушку для загрузчика UEFI.

5. Создаём образ ESP, создаём в нём директории, копируем в него файл bootx64.efi.

6. Создаём структуру из файлов и директорий, которые будут размещены в образе ISO.

7. Создаём гибридный загрузочный образ ISO.

8. Экспериментируем и проверяем работу образа в различных эмуляторах, например qemu, виртуальных машинах (VMWare, Virtual Box), записываем образ на оптический диск и флеш-накопитель (при помощи программ Win32DiskImager, balenaEtcher).

Например, чтобы запустить в qemu-эмуляторе загрузочный ISO с использованием BIOS, если бы он был записан на оптический диск:

Чтобы запустить в qemu-эмуляторе загрузочный ISO с использованием UEFI, если бы он был записан на флеш-накопитель:

Чтобы выйти из эмулятора, нужно нажать клавиши Ctrt + A, а потом X.

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

Ещё один способ создания загрузочного флеш-накопителя в Windows

Внимательный читатель мог заметить, что зачем-то в ISO-образ помимо образа ESP помещается и файл bootx64.efi. Напрашивается вопрос, зачем это делается, если UEFI нужен именно образ ESP в ISO-образе? Ответ заключается в том, что, разместив файл в образе ISO, можно создать загрузочный флеш-накопитель путём простого копирования всех файлов из ISO-образа.

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

  1. Запускаем программу diskpart.
  2. Выводим на экран список доступных дисков командой list disk.
  3. Выбираем среди них флеш-накопитель, который мы будем размечать и форматировать командой select disk.
  4. Выполняем очистку выбранного диска командой clean.
  5. Создаём первичный раздел командой create partition primary.
  6. Форматируем раздел командой format fs=fat32 quick.
  7. Выходим из diskpart командой exit.
  8. Копируем файлы из ISO-образа на флеш-накопитель простым копированием файлов

В нашем случае нам достаточно скопировать только директорию EFI, так как остальные файлы использоваться не будут, но для простоты скопируйте все файлы из ISO-образа.

Изучение структуры созданного загрузочного ISO-образа

Созданный образ у нас находится в директории out и имеет имя bootdisk.iso.

1. Выведем номер сектора, где располагается Booting Catalog:

Здесь 4 — это количество байт, которые выводим, 17 — номер сектора с Boot Record Volume Descriptor, 2048 — размер сектора, 71 — смещение внутри сектора, где располагается LBA Booting Catalog.

Номер сектора занимает 4 байта, поэтому нужно знать, какой порядок байтов используется Big Endian или Little Endian. Я не нашёл в спецификации El Torito, какой порядок используется, но, судя по всему, это Little Endian, поэтому байты нужно читать справа налево, получая 0x00000021.

2. Выведем первые четыре записи Booting Catalog:

Здесь 32 — это размер записи в Booting Catalog, 4 — количество записей, $(expr $(echo $((0x21))) — десятичное представление 0x21, 2048 — размер сектора.

3. Выведем LBA-адрес сектора, который будет загружен при загрузке средствами BIOS:

Здесь 4 — количество байт, которые выводим, 32 — размер записи в Booting Catalog (мы берём данные из Default Entry, поэтому пропускаем 32 байта), 8 — смещение внутри Default Entry, где располагается LBA загрузчика для BIOS, 2048 — размер сектора.

4. Выведем первые 512 байт этого сектора (это наша программа, которую мы написали на ассемблере и откомпилировали).

5. Выведем LBA сектора, где располагается образ раздела ESP, необходимый для загрузки средствами UEFI.

6. Выведем первые 512 байт образа ESP.

LBA адресов теоретически могут быть у вас быть другие. Я привёл те, которые были у меня.

Вы можете получить LBA адресов, по которым располагается откомпилированная программа на ассемблере, используя структуры MBR и GPT, находящиеся в первых секторах, изучив документацию по MBR и GPT. Здесь я приводить не буду, оставляю на самостоятельное изучение. Скажу только, что они будут отличаться, так как используются в этом случае сектора по 512 байт, а не по 2048.

Создание среды для сборки

Для создания воспроизводимого окружения для сборки очень удобно использовать Docker. Он позволяет создавать изолированные окружения с необходимыми зависимостями. Мы тоже будем использовать его, ниже я привожу Dockerfile.

Вы можете собрать образ Docker со всеми необходимыми для сборки зависимостями, используя команду.

Для запуска контейнера в интерактивном режиме в PowerShell в директории, где у вас содержатся файлы bioshello.asm и efihello.c нужно ввести команду.

Автоматизация сборки ISO-образа

Для автоматизации сборки из исходников существует множество различных утилит, но для простоты мы будем использовать make.

Основное отличие утилиты make от обычных shell-скриптов заключается в том, что она:

  • отслеживает зависимости в исходных файлах;
  • пересобирает только при наличии изменений.

Данный Makefile позволяет собрать ISO-образ (make clean all), а также запустить его в эмуляторе с BIOS, UEFI, с флеш-накопителя и cdrom, что понятно из названий.

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

    . . . . — программа для форматирования (форматирует USB-накопители тоже). Иногда помогает, когда программы записи образов отказываются записывать образ. — программа для записи образов на флеш-накопитель. — программа для записи образов на флеш-накопитель. Иногда записывает, когда и balenaEtcher отказывается. — сайт с информацией по разработке ОС, где среди прочего есть информация по загрузке ОС. — сайт с исчерпывающей информацией по операционным системам, но лучше искать информацию на нём с помощью Google.


В статье создан минимальный гибридный ISO-образ, который позволяет понять основы загрузки на компьютерах (BIOS и UEFI c жёсткого диска, флеш-накопителя, оптического диска). Многие вопросы не были рассмотрены с целью упрощения, так как статья имеет познавательный, а не энциклопедический характер и не рассчитана на тех, кто имеет значительный опыт по этой теме.

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

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

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

Процесс загрузки EFI

Платформы с UEFI осуществляют чтение таблицы разделов и подключают раздел ESP (EFI System Partition) — раздел VFAT с уникальным идентификатором GUID (Globally Unique Identifier). ESP содержит приложения EFI — загрузчики и служебные программы. В Red Hat Enterprise Linux 6 это раздел /boot/efi/ , а программы EFI расположены в /boot/efi/EFI/redhat/ .

/boot/efi/EFI/redhat/ содержит grub.efi — версию загрузчика, скомпилированную специально для EFI. В самом простом случае менеджер загрузки EFI выбирает этот файл в качестве загрузчика и загружает его в память.

Если раздел ESP содержит другие программы EFI, менеджер загрузки предложит выбрать программу для выполнения.

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

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

Booting an OS using UEFI

UEFI firmware does not support booting through the above mentioned method which is the only way supported by BIOS. UEFI has support for reading both the partition table as well as understanding filesystems.

The commonly used UEFI firmwares support both MBR and GPT partition table. EFI in Apple-Intel Macs are known to support Apple Partition Map also apart from MBR and GPT. Most of the UEFI firmwares have support for accessing FAT12 (floppy disks) , FAT16 and FAT32 filesystems in HDD and ISO9660 (and UDF) in CD/DVDs. EFI in Apple-Intel Macs can access HFS/HFS+ filesystems also apart from the mentioned ones.

UEFI does not launch any boot code in the MBR whether it exists or not. Instead it uses a special partition in the partition table called «EFI SYSTEM PARTITION» in which files required to be launched by the firmware are stored. Each vendor can store its files under <EFI SYSTEM PARTITION>/EFI/<VENDOR NAME>/ folder and can use the firmware or its shell (UEFI shell) to launch the boot program. An EFI System Partition is usually formatted as FAT32.

Under UEFI, every program whether they are OS loaders or some utilities (like memory testing apps) or recovery tools outside the OS, should be a UEFI Application corresponding to the EFI firmware architecture. Most of the UEFI firmware in the market, including recent Apple Macs use x86_64 EFI firmware. Only some older macs use i386 EFI firmware while no non-Apple UEFI system is known to use i386 EFI firmware.

A x86_64 EFI firmware does not include support for launching 32-bit EFI apps unlike the 64-bit Linux and Windows which include such support. Therefore the bootloader must be compiled for that architecture correctly.

Multibooting on UEFI

Since each OS or vendor can maintain its own files within the EFI SYSTEM PARTITION without affecting the other, multi-booting using UEFI is just a matter of launching a different UEFI application corresponding to the particular OS’s bootloader. This removes the need for relying on chainloading mechanisms of one bootloader to load another to switch OSes.

Linux Windows x86_64 UEFI-GPT Multiboot

Windows Vista (SP1+) and 7 pr 8 x86_64 versions support booting natively using UEFI firmware. But for this they need GPT partitioning of the disk used for UEFI booting.Windows x86_64 versions support either UEFI-GPT booting or BIOS-MBR booting.Windows 32-bit versions support only BIOS-MBR booting. Follow the instructions provided in the forum link given in the references sections as to how to do this. See;EN-US;2581408 for more info.

This limitation does not exist in Linux Kernel but rather depends on the bootloader used.For the sake of Windows UEFI booting, the Linux bootloader used should also be installed in UEFI-GPT mode if booting from the same disk.

Boot Process under UEFI

  1. System switched on — Power On Self Test, or POST process.
  2. UEFI firmware is loaded.
  3. Firmware reads its Boot Manager to determine which UEFI application to be launched and from where (ie. from which disk and partition).
  4. Firmware launches the UEFI application from the FAT32 formatted UEFISYS partition as defined in the boot entry in the firmware’s boot manager.
  5. UEFI application may launch another application (in case of UEFI Shell or a boot manager like rEFInd) or the kernel and initramfs (in case of a bootloader like GRUB) depending on how the UEFI application was configured.

Detecting UEFI Firmware Arch

If you have a non-mac UEFI system, then you have a x86_64 (aka 64-bit) UEFI 2.x firmware.

Some of the known x86_64 UEFI 2.x firmwares are Phoenix SecureCore Tiano, AMI Aptio, Insyde H2O.

Some of the known systems using these firmwares are Asus EZ Mode BIOS (in Sandy Bridge P67 and H67 motherboards), MSI ClickBIOS, HP EliteBooks, Sony Vaio Z series, many Intel Server and Desktop motherboards

Pre-2008 Macs mostly have i386-efi firmware while >=2008 Macs have mostly x86_64-efi. All macs capable of running Mac OS X Snow Leopard 64-bit Kernel have x86_64 EFI 1.x firmware.

To find out the arch of the efi firmware in a Mac, boot into Mac OS X and type the following command

If the command returns EFI32 then it is i386 EFI 1.x firmware. If it returns EFI64 then it is x86_64 EFI 1.x firmware. Macs do not have UEFI 2.x firmware as Apple’s EFI implementation is not fully compliant with UEFI Specification.

UEFI Support in Linux Kernel

Linux Kernel config options for UEFI

The required Linux Kernel configuration options for UEFI systems are :

UEFI Runtime Variables/Services Support — ‘efivars’ kernel module . This option is important as this is required to manipulate UEFI Runtime Variables using tools likeefibootmgr.

GUID Partition Table GPT config option — mandatory for UEFI support

UEFI Variables Support

UEFI defines variables through which an operating system can interact with the firmware.UEFI Boot Variables are used by the boot-loader and used by the OS only for early system start-up. UEFI Runtime Variables allow an OS to manage certain settings of the firmware like the UEFI Boot Manager or managing the keys for UEFI Secure Boot Protocol etc.

Access to UEFI Runtime services is provided by «efivars» kernel module which is enabled through the CONFIG_EFI_VAR=m kernel config option. This module once loaded exposes the variables under the directory /sys/firmware/efi/vars . One way to check whether the system has booted in UEFI boot mode is to load the «efivars» kernel module and check for the existence of /sys/firmware/efi/vars directory with contents similar to :

The UEFI Runtime Variables will not be exposed to the OS if you have used «noefi» kernel parameter in the boot-loader menu. This parameter instructs the kernel to completely ignore UEFI Runtime Services.

Userspace Tools

There are few tools that can access/modify the UEFI variables, namely

  1. efibootmgr — Used to create/modify boot entries in the UEFI Boot Manager —efibootmgr or efibootmgr-git
  2. uefivars — simply dumps the variables — uefivars-git — uses efibootmgr library
  3. Ubuntu’s Firmware Test Suite — fwts — fwts-git — uefidump command — fwts uefidump

Non-Mac UEFI systems


Initially the user may be required to manually launch the boot-loader from the firmware itself (using maybe the UEFI Shell) if the UEFI boot-loader was installed when the system is booted in BIOS mode. Then efibootmgr should be run to make the UEFI boot-loader entry as the default entry in the UEFI Boot Manager.

To use efibootmgr, first load the ‘efivars’ kernel module:

If you get no such device found error for this command, that means you have not booted in UEFI mode or due to some reason the kernel is unable to access UEFI Runtime Variables (noefi?).

Verify whether there are files in /sys/firmware/efi/vars/ directory. This directory and its contents are created by «efivars» kernel module and it will exist only if you have booted in UEFI mode, without the «noefi» kernel parameter.

If /sys/firmware/efi/vars/ directory is empty or does not exist, then efibootmgr command will not work. If you are unable to make the ISO/CD/DVD/USB boot in UEFI mode try#Create_UEFI_bootable_USB_from_ISO.

Assume the boot-loader file to be launched is /boot/efi/EFI/gummiboot/gummibootx64.efi . /boot/efi/EFI/gummiboot/gummibootx64.efi can be split up as /boot/efi and /EFI/gummiboot/gummibootx64.efi , wherein /boot/efi is the mountpoint of the UEFI System Partition, which is assumed to be /dev/sdXY (here X and Y are just placeholders for the actual values — eg:- in /dev/sda1 , X=a Y=1).

To determine the actual device path for the UEFI System Partition (should be in the form /dev/sdXY ), try :

Then create the boot entry using efibootmgr as follows :

In the above command /boot/efi/EFI/gummiboot/gummibootx64.efi translates to /boot/efi and /EFI/gummiboot/gummibootx64.efi which in turn translate to drive /dev/sdX -> partition Y -> file /EFI/gummiboot/gummibootx64.efi .

UEFI uses backward slash as path separator (similar to Windows paths).

The ‘label’ is the name of the menu entry shown in the UEFI boot menu. This name is user’s choice and does not affect the booting of the system. More info can be obtained from efibootmgr GIT README .

FAT32 filesystem is case-insensitive since it does not use UTF-8 encoding by default. In that case the firmware uses capital ‘EFI’ instead of small ‘efi’, therefore using \EFI\gummiboot\gummibootx64.efi or \efi\gummiboot\gummibootx64.efi does not matter (this will change if the filesystem encoding is UTF-8).

Linux Bootloaders for UEFI

Create an UEFI System Partition in Linux

For GPT partitioned disks

  • Using GNU Parted/GParted: Create a FAT32 partition. Set «boot» flag on for that partition.
  • Using GPT fdisk (aka gdisk): Create a partition with gdisk type code «EF00». Then format that partition as FAT32 using mkfs.vfat -F32 /dev/<THAT_PARTITION>

For MBR partitioned disks

  • Using GNU Parted/GParted: Create FAT32 partition. Change the type code of that partition to 0xEF using fdisk, cfdisk or sfdisk.
  • Using fdisk: Create a partition with partition type 0xEF and format it as FAT32 using mkfs.vfat -F32 /dev/<THAT_PARTITION>

UEFI Shell

The UEFI Shell is a shell/terminal for the firmware which allows launching uefi applications which include uefi bootloaders. Apart from that, the shell can also be used to obtain various other information about the system or the firmware like memory map (memmap), modifying boot manager variables (bcfg), running partitioning programs (diskpart), loading uefi drivers, editing text files (edit), hexedit etc.

UEFI Shell download links

You can download a BSD licensed UEFI Shell from Intel’s Tianocore UDK/EDK2 project.

Shell 2.0 works only in UEFI 2.3+ systems and is recommended over Shell 1.0 in those systems. Shell 1.0 should work in all UEFI systems irrespective of the spec. version the firmware follows. More info at ShellPkg and this mail

Launching UEFI Shell

Few Asus and other AMI Aptio x86_64 UEFI firmware based motherboards (from Sandy Bridge onwards) provide an option called «Launch EFI Shell from filesystem device» . For those motherboards, download the x86_64 UEFI Shell and copy it to your UEFI SYSTEM PARTITION as <UEFI_SYSTEM_PARTITION>/shellx64.efi (mostly /boot/efi/shellx64.efi ) .

Systems with Phoenix SecureCore Tiano UEFI firmware are known to have embedded UEFI Shell which can be launched using either F6, F11 or F12 key.

Important UEFI Shell Commands

UEFI Shell commands usually support -b option which makes output pause after each page. map lists recognized filesystems ( fs0 , ...) and data storage devices ( blk0 , ...). Run help -b to list available commands.

BCFG command is used to modify the UEFI NVRAM entries, which allow the user to change the boot entries or driver options. This command is described in detail in page 83 (Section 5.3) of «UEFI Shell Specification 2.0» pdf document.

To dump a list of current boot entries —

To add a boot menu entry for rEFInd (for example) as 4th (numbering starts from zero) option in the boot menu

where fs0: is the mapping corresponding to the UEFI System Partition and \EFI\arch\refind\refindx64.efi is the file to be launched.

To remove the 4th boot option

To move the boot option #3 to #0 (i.e. 1st or the default entry in the UEFI Boot menu)

For bcfg help text

EDIT command provides a basic text editor with an interface similar to nano text editor, but slightly less functional. It handles UTF-8 encoding and takes care or LF vs CRLF line endings.

To edit, for example rEFInd’s refind.conf in the UEFI System Partition (fs0: in the firmware)

Type Ctrl-E for help.

Hardware Compatibility

Create UEFI bootable USB from ISO

First create a MBR partition table in the USB using fdisk. Mount the USB partition and create a FAT32 filesystem with LABEL as used in the Archiso configuration.

Obtain the label from /mnt/iso/loader/entries/archiso-x86_64.conf ; this is used by the archiso hook in initramfs to identify the udev path to the installation media.

If you find the error: «No loader found. Configuration files in /loader/entries/*.conf are needed.« A possible fix is to use a different uefi bootloader to the included one, gummiboot.

Download refind-efi pkg and extract the file /usr/lib/refind/refind_x64.efi from within the package to (USB)/EFI/boot/bootx64.efi (overwrite or rename any existing (USB)/EFI/boot/bootx64.efi file).

Then copy this text to EFI/boot/refind.conf . Take care that the label in the Arch menu section ( ARCH_201302 here) matches that of your usb’s.

You should now be able to successfully boot, and you can choose which EFI you’d like to load.

Remove UEFI boot support from ISO

Most of the 32-bit EFI Macs and some 64-bit EFI Macs refuse to boot from a UEFI(X64)+BIOS bootable CD/DVD. If one wishes to proceed with the installation using optical media, it might be necessary to remove UEFI support first.

Mount the official installation media and obtain the archisolabel as shown in the previous section.

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

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