Pull to refresh

Linux Kernel EFI Boot Stub или «Сам себе загрузчик»

Reading time10 min
Views77K
UEFI Tux Logo

Введение


Прочитав недавнюю статью Загрузка ОС Linux без загрузчика, понял две вещи: многим интересна «новинка», датируемая аж 2011 годом; автор не описал самого основного, без чего, собственно, и работать ничего не будет в некоторых случаях. Также была ещё одна статья, но либо она уже устарела, либо там опять таки много лишнего и недосказанного одновременно.

А конкретно, был упущен основной момент — сборочная опция ядра CONFIG_EFI_STUB. Так как в последних версиях U(lu/ku/edu/*etc*)buntu эта опция по умолчанию уже включена, никаких подозрений у автора не появилось.
Насколько мне известно, на текущий момент она включена в дистрибутивах указанных версий и выше: Arch Linux, Fedora 17, OpenSUSE 12.2 и Ubuntu 12.10. В комментах ещё упомянули, что Debian с ядром 2.6 умеет, но это не более, чем бэкпорт с последних версий. На этих дистрибутивах пересобирать вообще ничего не нужно! А ведь на других CONFIG_EFI_STUB, скорее всего, либо вообще отсутствует, т. к. опция доступна только с ядра версии 3.3.0 и выше, либо выключена по умолчанию. Соответственно, всё, описанное ниже, справедливо для ядра, собранного с опцией CONFIG_EFI_STUB.

Итак, что же такое Linux Kernel EFI Boot Stub?


Общая информация

А ни что иное, как… «exe-файл»! Да-да, «виндовый» PE/COFF. Ну, а точнее, только закос под него с небольшими модификациями, чтобы угодить загрузчику UEFI. Можно убедиться в этом, прочитав первые 2 байта ядра:
$ od /boot/vmlinuz-linux --address-radix=x --read-bytes=2 -t x1c
0000000  4d  5a
          M   Z
0000002

Знакомо, не правда ли? Как минимум тем, кто хоть раз «для интереса» открывал исполняемый файл MS-DOS или Windows в блокноте, хекс-редакторе или чём-то покруче. Это инициалы Марка Збиковски, который, собственно, и разработал данный формат файлов в MS-DOS. Сигнатура этой заглушки до сих пор висит рудиментом в современных исполнимых файлах Windows, сжирая со своим заголовком целых 64 байта на каждый файл!

DOS-заголовок попадает на legacy-код, который выполняется в случае загрузки ядра как бут-сектора, и ругается на манеру MS-DOS при запуске PE-файлов: «Direct floppy boot is not supported. Use a boot loader program instead. Remove disk and press any key to reboot ...». Поэтому информация из этого заголовка здесь является мусором, кроме, собственно, сигнатуры 'MZ' и адреса смещения следующего заголовка.

Идём дальше.
Спецификация PE/COFF говорит нам, что по смещению 0x3c находится 32-битное смещение второго заголовка с сигнатурой «PE\0\0»:
$ od /boot/vmlinuz-linux --address-radix=x --read-bytes=4 --skip-bytes=0x3c -t x4
00003c 000000b8
000040

итак, смещение равно 0xb8, что справедливо для текущего stable-ядра x86_64 архитектуры, на x86 будет 0xa8. Читаем:
$ od /boot/vmlinuz-linux --address-radix=x --read-bytes=4 --skip-bytes=0xb8 -t x1c
0000b8  50  45  00  00
         P   E  \0  \0
0000bc

А вот и сигнатура второго заголовка! Как можно было догадаться, это аббревиатура от словосочетания Portable Executable, с которой и начинается полезная нагрузка в исполнимых файлах.

Даже загрузчик Windows плевал на половину полей этого заголовка, а уж UEFI они и вовсе не нужны, поэтому некоторые из них прописаны статически, важные же — заполняются во время сборки ядра. Множество «ненужных» полей, всяких таймстемпов, котрольных сумм и пр. просто остаются нулями. Заполняются в основном размеры, смещения, точка входа и т. д. Поэтому, можно с натяжкой назвать данный PE-файл полностью валидным. Однако, классические утилиты LordPE или PETools вполне себе довольствуются сигнатурами и рассказывают о файле всё, что им известно:

PE optional header


imageОсновное отличие от «реальных» исполняемых файлов в Windows — это флаг Subsystem опционального заголовка, который выставляется в IMAGE_SUBSYSTEM_EFI_APPLICATION, а не в IMAGE_SUBSYSTEM_WINDOWS_GUI для графических или IMAGE_SUBSYSTEM_WINDOWS_CUI для символьных (консольных) приложений Windows соответственно.

Структура

В общем же, всё как в обычном PE-файле. На текущий момент стабильной версии 3.11.4 ядра Arch Linux из репозиториев, в нём содержатся 3 секции: '.setup', '.reloc' и '.text'.

  • Секция .setup, содержит в основном legacy-код для инициализации в случае загрузки в режиме совместимости. При загрузке же в UEFI mode, все переключения режимов процессора, начальные инициализации производит прошивка.
  • .reloc секция обязательно требуется загрузчиком, поэтому при сборке ядра создаётся пустая заглушка «чтоб было».
  • Самая интересная секция .code, собственно, содержит EntryPoint и основной код всего остального ядра. После того, как EFI-application найдено, загрузчик выполняет загрузочный сервис LoadImage, тем самым загружая весь образ в память. Тип резидентности зависит от поля Subsystem: EFI_APPLICATION будет выгружаться, когда отработает. EFI_DRIVER же может быть Unloadable и выгрузится только в случае критической ошибки. Далее передаётся управление на точку входа, обычно это функция efi_main() — аналог main() в C.

На самом деле, я немного слукавил, в начале назвав ядро exe-файлом. По сути это простое себе приложение EFI, которое использует формат PE32+.

Основные требования


imageПрежде всего, необходимо активировать режим загрузки EFI-mode. Пункт может называться как вдумается вендору, обычно находится во вкладке Boot Options. Если увидели там что-то вроде Legacy Mode или CSM (Compatibility Support Mode), либо просто BOIS Mode, меняйте на что-то похожее: (U)EFI Mode, Enhanced Mode или Advanced Mode.

Если материнская плата имеет логотип «Windows 8 Ready!», то, скорее всего, режим EFI Boot Mode уже активирован по умолчанию.

В большинстве случаев, для загрузки ядра Linux в EFI-mode необходимо выключить опцию Secure Boot.

Разметка диска

Многие источники указывают, что обязательно нужна разметка диска GPT, а не MBR, но это не так. UEFI вполне себе умеет MBR. Другое дело, например, Windows насильно заставляет разбивать диск новым методом, чтобы грузиться в EFI mode и ругается на древность Master Boot Record'а. И правильно делает! Разметив диск «современно» ничего не потеряем, а только выиграем.
Во-первых, не будет проблем со всякими там Primary/Logical разделами, «туда не ходи — сюда ходи» и прочими рудиментами.
Во-вторых, хоть сейчас и продвигаются массово SolidState-диски, у которых объёмы пока не сильно удивляют, размером же обычной «вертушки» в несколько терабайт сейчас уже никого не удивишь. А ведь под MBR можно разметить раздел максимум около 2ТБ. GPT же, видит ну очень много, можно даже цифру не называть — относительно не скоро появятся диски таких размеров.
Ну и плюс всякие бонусы, типа дублирования GPT-записи в начале и конце диска, контрольные суммы целостности и т. п., добавляют желания не раздумывая размечать диск под GPT.

Статей, как разбить диск с помощью различных утилит в GNU/Linux можно найти огромное количество.

Отдельный раздел

Тип раздела

Спустя nn-цать лет разработки стандартов, инженеры таки решили, что хардкодить — не есть хорошо. Теперь не важно где находится наш загрузочный раздел, загрузчик UEFI делает очень просто: он перебирает все подряд разделы и диски и ищет один особенный. Особенность его заключается в том, что в случае с MBR-разметкой, он имеет тип с кодом 0xEF (как можно догадаться, от EFI). В случае разметки GPT — раздел с GUID равным C12A7328-F81F-11D2-BA4B-00A0C93EC93B.

Здесь существует некоторая неявность. Все утилиты для разметки, например, parted, имеют свойство установки и отображения флага «boot», который применяется к разделу. Так вот, в случае с MBR, такая возможность действительно имеется, т. е. существует реальный байт, который указывает БИОСу на «загрузочность» раздела. Этот флаг можно поставить на любой раздел, MBR которого, мы хотим скормить БИОСу для загрузки. Но, когда мы имеем дело с GPT, никакого флага в действительности нет! Под этим флагом parted понимает как раз GUID равный вышеуказанному. Т. е. по факту GPT boot flag = GPT EFI Partition!
parted
# parted /dev/sda -l
Модель: ATA ST3750330AS (scsi)
Диск /dev/sda: 750GB
Размер сектора (логич./физич.): 512B/512B
Таблица разделов: gpt
Disk Flags:

Номер  Начало  Конец   Размер  Файловая система  Имя               Флаги
 1     1049kB  135MB   134MB   fat32             EFI System        загрузочный
 2     135MB   269MB   134MB   ext2              Linux filesystem
 3     269MB   8859MB  8590MB  linux-swap(v1)    Linux swap
 4     8859MB  30,3GB  21,5GB  ext4              Linux filesystem
 5     30,3GB  46,4GB  16,1GB  ext4              Linux filesystem
 6     46,4GB  67,9GB  21,5GB  ext4              Linux filesystem
 7     67,9GB  750GB   682GB   xfs               Linux filesystem
gdisk же, не страдает этим:
gdisk
# gdisk /dev/sda -l
GPT fdisk (gdisk) version 0.8.7

Partition table scan:
  MBR: protective
  BSD: not present
  APM: not present
  GPT: present

Found valid GPT with protective MBR; using GPT.
Disk /dev/sda: 1465149168 sectors, 698.6 GiB
Logical sector size: 512 bytes
Disk identifier (GUID): 02D11900-D331-4114-A3D7-8493969EF533
Partition table holds up to 128 entries
First usable sector is 34, last usable sector is 1465149134
Partitions will be aligned on 2048-sector boundaries
Total free space is 2014 sectors (1007.0 KiB)

Number  Start (sector)    End (sector)  Size       Code  Name
   1            2048          264191   128.0 MiB   EF00  EFI System
   2          264192          526335   128.0 MiB   8300  Linux filesystem
   3          526336        17303551   8.0 GiB     8200  Linux swap
   4        17303552        59246591   20.0 GiB    8300  Linux filesystem
   5        59246592        90703871   15.0 GiB    8300  Linux filesystem
   6        90703872       132646911   20.0 GiB    8300  Linux filesystem
   7       132646912      1465149134   635.4 GiB   8300  Linux filesystem


Вывод: если наш EFI-раздел на MBR — ставим тип раздела EFI Partition и boot flag. Если GPT — либо тип EFI Partition, либо boot flag, так как они представляют собой одно и то же.

Есть ещё всякие вещи, типа GPT legacy boot flag, который устанавливается в Protective MBR, и прочее, но всё это костыли, которые используются только в режиме совместимости. В режиме GPT UEFI Boot должны игнорироваться.

Файловая система

В разных источниках пишут по-разному. Кто-то говорит, что FAT16 можно использовать, кто-то даже FAT12 рекомендует. Но, не лучше ли последовать совету официальной спецификации? А она говорит, что системный раздел должен быть в FAT32. Для removable-media (USB HDD, USB Flash) — ещё и FAT12/FAT16 в придачу к FAT32.
Про размер раздела ничего не говорится. Однако, по причине начальных костыльных и баганых реализаций загрузчиков и прошивок, опытным путём народ выяснил, что во избежание различных «внезапностей», рекомендуется размер не менее 520МиБ (546МБ). Здесь как повезёт, проблем может не быть и с 32-мегабайтным разделом.

Структура директорий

После того, как загрузчик нашёл свой «меченый» раздел и убедился, что поддерживает файловую систему, он начинает выполнять все действия с путями, относительно корня раздела. Кроме того, все файлы в данном разделе должны находиться в директории \EFI\, которая, в свою очередь, является единственной в корне раздела. По соглашению, каждому вендору рекомендуется выделить себе папку с уникальным названием и поместить её в \EFI\, например: \EFI\redhat\, \EFI\microsoft\, \EFI\archlinux\. В директории вендора находятся непосредственно исполнимые efi-приложения. Рекомендуется один файл на одну архитектуру. Файлы должны иметь расширение .efi.

Для съёмных устройств предназначена директория \EFI\BOOT\. В ней так же рекомендуется не более одного файла для каждой архитектуры. В дополнение к этому, файл должен называться boot{arch}.efi. Например, \EFI\BOOT\bootx64.efi. Доступные архитектуры: ia32, x64, ia64, arm, aa64.

Доступ к NVRAM

По умолчанию, если ничего не записано в энергонезависимой памяти UEFI, будет загружаться \EFI\BOOT\bootx64.efi. Чтобы записать в NVRAM путь к необходимому приложению, можно воспользоваться утилитой efibootmgr. Попробуем вывести текущие записи:
# efibootmgr -v

В некоторых дистрибутивах для работы этой утилиты требуется включенная опция ядра CONFIG_EFI_VARS.

Приступаем


imageИтак, мы разметили FAT32 EFI System Partition (ESP) размером 550МиБ. Либо, у нас стоит винда второй системой и уже сама создала его. Правда, создаёт она его обычно размером около 100МБ, но лично у меня проблем никогда не возникало.
В /boot уже имеется ядро с поддержкой EFI boot STUB.
Проверить
Чтобы проверить, была ли включена опция при сборке ядра, выполним:
$ zgrep CONFIG_EFI_STUB /proc/config.gz
либо
$ zgrep CONFIG_EFI_STUB /boot/config-`uname -r`

CONFIG_EFI_STUB=y означает, что опция активна.

Далее у нас куча вариантов развития событий:
  • Можно смонтировать ESP\{vendor}\ на /boot через mount --bind, предварительно скопировав содержимое.
    Исключения
    Данный пункт подходит только для дистрибутивов, которые не содержат символические ссылки в каталоге /boot. Например, на openSUSE монтировать не удастся, т. к. он содержит там несколько ссылок, в том числе и на само ядро.
  • Как вариант, при обновлении ядра, каждый раз копировать его и ram-disk в ESP\{vendor}\
  • Можно поставить EFI driver на чтение файловой системы /boot и грузиться напрямую, лишь добавив к ядру расширение '.efi' (а лучше хардлинк).

Теперь нужно как-то добавить загрузочный пункт в NVRAM UEFI. Здесь снова множество вариантов:

Если мы уже загружены в режиме EFI (efibootmgr -v не ругается) с помощью загрузчиков GRUB2, rEFInd и т. п., то всё нормально:
  • Используем efibootmgr, который умеет передавать параметры ядра.
  • Если efibootmgr брыкается, можно воспользоваться UEFI Shell, который, как и наше ядро, является EFI-приложением. Через его команду bcfg возможно редактировать пункты загрузки.
  • Может быть такой вариант: efibootmgr ругается на добавление параметров, значит прошивка не поддерживает их запись (либо просто кривая, что вероятнее). В прошлой статье в комментах упомянули параметр ядра efi_no_storage_paranoia, который может помочь. Но пользоваться им можно только если вы уверены в том, что ваша прошивка реализована полностью в соответствии со спецификацией! Разработчики предупреждают, что если вендор добавил костылей и отсебятины при реализации, есть неиллюзорная вероятность материализовать кирпич на месте материнской платы.
  • Можно также грузиться через UEFI Shell. Для него создаётся скрипт startup.nsh, в котором указывается команда загрузки ядра с нужным command line. А Shell, в свою очередь, добавляется как пункт загрузки.
  • Существует ещё одна проблема: добавление пункта возможно только для одного пути ядра, при этом ram-disk не видится. В большинстве статей рекомендуют при этом пересобирать ядро со встроенным initrd. Точно не знаю — проблема ли ядра это, или загрузчика. Но на текущий момент в 90% случаев всё поддерживается и пересобирать ядро не нужно.
    Вероятная причина заблуждения
    Рекомендации по встраиванию рам-диска в ядро пошли, скорее всего, из-за массового неверного указания пути к нему. В ранних реализациях EFI Boot Stub, ядро не плевалось ошибкой о неверном пути к ram-disk, а молча отказывалось грузиться. Видимо поэтому все начали массово внедрять его в ядро, решив, что он не поддерживается. Хотя поддержка параметра initrd существует с самого появления в ядре фичи Boot Stub.

    ВАЖНО: Путь к ram-диску передаётся абсолютный через обратные слеши " \ ", а не прямые! Например, initrd=\EFI\archlinux\initramfs-linux.img.
    Исключения для анархистов
    На самом деле, ядра, версий выше 3.8.0-rc5 не видят разницы между прямым и обратным слешем — будет работать любой. Но вот с момента появления фичи Boot Stub в версии 3.2.0-rc5, путь, записанный через прямые слеши, ядро просто не видело и молча без ошибок отказывалось грузиться. Ругаться ошибками по этому поводу, оно научилось в версии 3.4.0.

Если же мы только узнали про режим EFI boot и хотим на него перейти, чтобы добавлять пункты загрузки, нужно уже находиться в данном режиме, а мы то всё ещё в режиме совместимости… Есть два основных решения:
  • Скачать первый попавшийся live-cd с поддержкой EFI boot. И из него уже воспользоваться командой efibootmgr.
  • Скачать UEFI Shell. Из него можно, как загрузиться в режиме EFI, просто указав ядро и ram disk, так и редактировать пункты загрузки.

Dualboot без загрузчика


Если у вас установлены 2 системы одновременно, и всё равно не хочется ставить сторонний загрузчик, можно добавить обе в пункты загрузки UEFI и подкорректировать предпочитаемый boot order. Загрузчик Windows обычно располагается в \EFI\Microsoft\BOOT\bootmgfw.efi.

Итого


Если всё сделано правильно, перегружаемся, вызываем Boot Menu, выбираем добавленный нами пункт и смотрим на почти мгновенную загрузку. В случае с SSD, FastBoot, Readahead и Arch Linux — около 3-4 секунд. Домашний сервер уже год загружается без всяких сторонних загрузчиков, используя EFI Boot STUB.
Конечно, выигрыш в скорости тут минимальный, но, как пишут знающие люди типа Roderick Smith, иногда в режиме EFI Boot происходит «более адекватная» инициализация оборудования, чем в режимах совместимости.

Заключение


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

Литература


Официальные спецификации UEFI
Roderick W. Smith's Web Page — творчество автора многих утилит, связанных с EFI, загрузчиками и разметкой диска.
ArchWiki: UEFI Bootloaders — бессменная и одна из лучших и полных вики по GNU/Linux одного из дистрибутивов.
Официальная спецификация PE/COFF
Tags:
Hubs:
+67
Comments23

Articles

Change theme settings