Введение
Прочитав недавнюю статью
Загрузка ОС 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 вполне себе
довольствуются сигнатурами и рассказывают о файле всё, что им известно:
Основное
отличие от «реальных» исполняемых файлов в 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+.
Основные требования
Прежде
всего, необходимо активировать режим загрузки 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-разметкой, он имеет тип с кодом
0xEF00 (как можно догадаться, от EFI). В случае разметки GPT — раздел с
GUID равным
C12A7328-F81F-11D2-BA4B-00A0C93EC93B.
Здесь существует некоторая неявность. Все утилиты для разметки,
например, parted, имеют свойство установки и отображения флага «boot»,
который применяется к разделу. Так вот, в случае с MBR, такая
возможность действительно имеется, т. е. существует реальный байт,
который указывает БИОСу на «загрузочность» раздела. Этот флаг можно
поставить на любой раздел, MBR которого, мы хотим скормить БИОСу для
загрузки. Но, когда мы имеем дело с GPT, никакого флага в
действительности нет! Под этим флагом parted понимает как раз GUID
равный вышеуказанному. Т. е. по факту GPT boot flag = GPT EFI Partition!
parted
gdisk же, не страдает этим:
gdisk
Вывод: если наш EFI-раздел на MBR — ставим тип раздела EFI Partition
и boot flag. Если GPT —
либо тип EFI Partition,
либо boot flag, так как они представляют собой одно и то же.
Есть ещё всякие странные вещи, типа GPT legacy boot flag, который
устанавливается в Protective MBR, и прочее, но всё это костыли, которые
стандартом не описаны.
Файловая система
В разных источниках пишут по-разному. Кто-то говорит, что 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.
ВАЖНО: обратите внимание, что пути для загрузчика в EFI-разделе указываются обратным слешем "\", а не прямым, как в Linux!
Доступ к NVRAM
По умолчанию, если ничего не записано в энергонезависимой памяти UEFI, будет загружаться
\EFI\BOOT\bootx64.efi. Чтобы записать в
NVRAM путь к необходимому приложению, можно воспользоваться утилитой efibootmgr. Попробуем вывести текущие записи:
В некоторых дистрибутивах для работы этой утилиты требуется включенная опция ядра CONFIG_EFI_VARS.
Приступаем
Итак, мы разметили FAT32 EFI System Partition (ESP) размером
550МиБ.
Либо, у нас стоит винда второй системой и уже сама создала его. Правда,
создаёт она его обычно размером около 100МБ, но лично у меня проблем
никогда не возникало.
В /boot уже имеется ядро с поддержкой EFI boot STUB.
Проверить
Далее у нас куча вариантов развития событий:
- Можно смонтировать ESP\{vendor}\ на /boot через mount --bind, предварительно скопировав содержимое.
- Как вариант, при обновлении ядра, каждый раз копировать его и 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, в свою очередь, добавляется как пункт загрузки.
Если же мы только узнали про режим 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