Загрузка системы: BIOS и UEFI, MBR/GPT, GRUB, systemd-boot, initramfs
Есть момент, когда операционной системы ещё нет. Процессор включился, регистры в неопределённом состоянии, оперативная память — электрический шум, диск существует как физическое устройство, но никто не знает, что такое «файл». И из этого состояния за несколько секунд надо прийти к работающему ядру, смонтированной файловой системе и процессу с PID 1.
Загрузка — это единственная часть операционной системы, которая строится снизу вверх,
каждый следующий уровень абстракции создаётся руками предыдущего. Всё остальное в ОС можно
изучать сверху: вы вызываете open(), и где-то там VFS. Здесь наоборот — сначала 512 байт
машинного кода, которые умеют ровно одну вещь: прочитать следующие 30 килобайт.
Зачем это прикладному программисту
Соблазн сказать «загрузка — дело админа» велик, но на практике эта тема прорастает в работу разработчика в пяти местах.
Образы и иммутабельная инфраструктура. Вы собираете AMI, qcow2 или образ для bare metal.
Всё, что в нём есть до /sbin/init — ваша зона ответственности: разметка, ESP, initramfs с
нужными драйверами, параметры ядра. Ошибка здесь не даёт стектрейса, она даёт чёрный экран.
Отладка «оно не поднялось». Инстанс не пришёл в сеть. Причина может быть в чём угодно
от порчи GPT после ресайза диска до отсутствия модуля virtio_blk в initramfs. Умение
прочитать вывод serial console и понять, на каком этапе всё встало, экономит часы.
Параметры ядра как рычаг производительности. mitigations=off, isolcpus, hugepages,
intel_iommu=on, nohz_full — всё это задаётся только на этапе загрузки и меняет поведение
вашего кода на десятки процентов. Как эти строки попадают в /proc/cmdline — сюжет этой статьи.
Скорость запуска. Serverless, автоскейлинг, CI-раннеры: если инстанс поднимается 40 секунд вместо 8, это прямые деньги. Половина этого времени обычно тратится до старта пользовательского кода.
Безопасность цепочки поставки. Secure Boot, measured boot, TPM-attestation, unattended дешифровка дисков — все они опираются на то, кто и что подписал на этапе загрузки. Если вы делаете систему, которая должна доказать «я запущена на неизменённой платформе», вам сюда.
Про то, что происходит после старта PID 1, — соседняя статья Системы инициализации; про то, что ядро делает с памятью после того, как получит управление, — Управление памятью.
Карта: две принципиально разные цепочки
Прежде чем нырять в детали, важно увидеть, что на x86 сегодня сосуществуют две несовместимые модели загрузки. Они отличаются не «форматом таблицы разделов», а тем, что прошивка умеет делать.
ME / PSP стартуют раньше вас"] MC --> FW{"Тип прошивки"} FW -->|"Legacy BIOS / CSM"| B1["POST: тест памяти, перебор устройств"] B1 --> B2["Читает LBA 0 первого загрузочного диска
ровно 512 байт"] B2 --> B3{"Последние 2 байта
= 0x55AA?"} B3 -->|"нет"| BERR["Next boot device / No bootable device"] B3 -->|"да"| B4["Копирует по адресу 0x7C00,
jmp туда в реальном режиме"] B4 --> B5["GRUB boot.img → core.img из MBR gap"] B5 --> COMMON FW -->|"UEFI"| U1["SEC → PEI → DXE → BDS фазы
инициализация чипсета, PCIe, USB"] U1 --> U2["Читает NVRAM: BootOrder, Boot0000…"] U2 --> U3["Монтирует ESP как FAT32,
загружает PE/COFF образ"] U3 --> U4{"Secure Boot включён?"} U4 -->|"да"| U5["Проверка подписи по db, отказ по dbx"] U4 -->|"нет"| U6["Просто запуск"] U5 --> U6 U6 --> U7["shim → GRUB / systemd-boot / UKI"] U7 --> COMMON COMMON["Загрузчик читает vmlinuz + initrd,
формирует boot_params и cmdline"] COMMON --> K1["Ядро: распаковка, start_kernel,
инициализация подсистем"] K1 --> K2["initramfs распакован в rootfs/tmpfs"] K2 --> K3["exec /init — первый пользовательский процесс"] K3 --> K4["Найти и смонтировать настоящий корень"] K4 --> K5["switch_root → /sbin/init, PID 1"]
Ключевая мысль: в legacy-модели прошивка не умеет ничего, кроме «прочитай мне сектор номер N». Она не знает, что такое файловая система, раздел или файл. Поэтому вся сложность переехала в загрузчик, а загрузчик пришлось резать на стадии, потому что первая стадия обязана поместиться в 440 байт. В UEFI-модели прошивка — это по сути маленькая ОС с драйверами FAT32, сети, видео и таймеров, поэтому «загрузчик» может быть обычным исполняемым файлом, а иногда его вообще нет.
Этап нулевой: до того, как вы что-либо контролируете
Разработчику полезно знать, что даже самый первый байт вашего кода — далеко не первый байт, исполненный в системе.
На x86 после снятия сигнала RESET процессор начинает выполнение по reset vector
0xFFFFFFF0 — за 16 байт до конца 4-гигабайтного адресного пространства, в реальном режиме,
но с «скрытой» базой сегмента, указывающей в ПЗУ. Там стоит jmp на настоящий код прошивки.
Но и это не начало. На современных платформах Intel до пробуждения основного ядра стартует Intel Management Engine — отдельный процессор со своей ОС (MINIX-подобный ME 11+), который инициализирует часть платформы и проверяет подписи. У AMD аналог — Platform Security Processor. Отсюда важный практический вывод: «полный контроль над машиной с первой инструкции» на современном x86 — иллюзия. Проекты вроде coreboot и Heads борются за то, чтобы уменьшить долю проприетарного кода, но полностью исключить его нельзя.
Микрокод. Процессор исполняет x86-инструкции не напрямую, а через микрокод, который можно обновлять. Обновление грузится дважды: прошивкой на раннем этапе и повторно ядром, чтобы не зависеть от версии BIOS. В Linux второй способ реализован как early cpio — несжатый архив в самом начале initramfs, который ядро распаковывает до инициализации CPU. Вот он на реальной машине:
$ lsinitramfs /boot/initrd.img-6.8.0-136-generic | head -9
.
kernel
kernel/x86
kernel/x86/microcode
kernel/x86/microcode/AuthenticAMD.bin
kernel
kernel/x86
kernel/x86/microcode
kernel/x86/microcode/GenuineIntel.bin
Обратите внимание: два независимых cpio-архива склеены конкатенацией, поэтому . и kernel
встречаются дважды. Это легальный формат — ядро читает их последовательно.
Legacy BIOS: 440 байт, в которые надо уложить жизнь
Разберём на живом примере. Машина ниже загружается через BIOS, диск размечен под MBR:
$ sudo fdisk -l /dev/sda
Disk /dev/sda: 240 GiB, 257698037760 bytes, 503316480 sectors
Disklabel type: dos
Disk identifier: 0xb3ed5c69
Device Boot Start End Sectors Size Id Type
/dev/sda1 * 2048 4194303 4192256 2G 83 Linux
/dev/sda2 4194304 503316479 499122176 238G 83 Linux
Прочитаем первый сектор напрямую — это и есть весь «BIOS-загрузчик»:
$ sudo dd if=/dev/sda bs=512 count=1 | xxd | head -3
00000000: eb63 9010 8ed0 bc00 b0b8 0000 8ed8 8ec0 .c..............
00000010: fbbe 007c bf00 06b9 0002 f3a4 ea21 0600 ...|.........!..
00000020: 00be be07 3804 750b 83c6 1081 fefe 0775 ....8.u........u
$ sudo dd if=/dev/sda bs=512 count=1 | xxd | tail -5
00000180: 7de8 2e00 cd18 ebfe 4752 5542 2000 4765 }.......GRUB .Ge
00000190: 6f6d 0048 6172 6420 4469 736b 0052 6561 om.Hard Disk.Rea
000001a0: 6400 2045 7272 6f72 0d0a 00bb 0100 b40e d. Error........
000001b0: cd10 ac3c 0075 f4c3 695c edb3 0000 8004 ...<.u..i\......
000001f0: 0000 0000 0000 0000 0000 0000 0000 55aa ..............U.
Читается почти как исходник. eb 63 — короткий jmp +0x63 через область данных.
be 00 7c bf 00 06 b9 00 02 f3 a4 — классика: mov si, 0x7C00; mov di, 0x0600; mov cx, 0x200; rep movsb, то есть код переносит сам себя с 0x7C00 на 0x0600, чтобы
освободить 0x7C00 для следующей стадии. Дальше ea 21 06 00 00 — дальний прыжок на новую
копию. В конце — три строки на весь запас диагностики: GRUB, Geom, Hard Disk,
Read, Error. Если вы когда-нибудь видели загадочное GRUB Read Error без единой
подробности — вот почему: на подробности не было байтов. И финальные 55 aa.
Байты 80 04 01 04 83 fe c2 ff ... начиная с 0x1BE — таблица разделов: 80 = активный,
дальше CHS-адрес (мёртвая архаика), 83 = тип «Linux».
Пространственная картина того, куда всё это ложится, и почему без переключения режимов дальше не уехать:
Здесь спрятана суть проблемы: ядро весит 15 МиБ, а адресуемой памяти в реальном режиме — один мегабайт, из которого свободно около 630 КиБ. Поэтому загрузчик обязан:
- поднять линию A20 (исторический костыль совместимости с 8086);
- построить GDT и переключиться в защищённый режим;
- читать ядро с диска через
int 0x13в реальном режиме кусками, каждый раз возвращаясь назад в реальный режим и обратно (unreal mode или переключения туда-сюда); - собрать
boot_paramsи прыгнуть в точку входа ядра.
Отсюда трёхстадийная архитектура GRUB. boot.img — те самые 440 байт, знающие только LBA
следующей стадии. core.img — 25–30 КиБ, лежит в MBR gap (сектора 1–2047), содержит
модули для чтения ext4, LVM, RAID, зашифрованных томов. И только потом GRUB может открыть
файл /boot/grub/grub.cfg как файл.
Типичная авария. MBR gap ничем не защищён — это просто «ничья» область диска. Если разметить диск с первым разделом на секторе 1 (старые утилиты так делали) или поставить поверх что-то, использующее начало диска, core.img затирается. Симптом:
error: unknown filesystemи приглашениеgrub rescue>. Именно поэтому современные утилиты выравнивают первый раздел на 1 МиБ (сектор 2048) — заодно это даёт правильное выравнивание под SSD.
MBR против GPT: не только про размер диска
MBR-таблица родилась в 1983 году в PC DOS 2.0 и несёт всю тяжесть возраста: четыре раздела,
32-битные номера секторов, никаких контрольных сумм. Ограничение в 2 ТиБ — прямое следствие
2^32 × 512 байт. «Расширенные разделы» — надстройка-костыль: один из четырёх слотов
объявляется контейнером и содержит связный список описателей, что делает операции над ними
хрупкими.
GPT из спецификации UEFI решает всё это по-инженерному аккуратно:
# Посмотреть GPT (на GPT-диске)
$ sudo gdisk -l /dev/nvme0n1
Partition table scan:
MBR: protective
GPT: present
Number Start (sector) End (sector) Size Code Name
1 2048 1050623 512.0 MiB EF00 EFI system partition
2 1050624 1000214527 476.4 GiB 8300 Linux filesystem
# Типы разделов — это GUID, а не байт. Полезные:
# EF00 C12A7328-F81F-11D2-BA4B-00A0C93EC93B EFI System Partition
# EF02 21686148-6449-6E6F-744E-656564454649 BIOS boot (для core.img на GPT!)
# 8304 4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709 Linux x86-64 root (для discoverable partitions)
Три вещи, которые стоит запомнить практику:
Protective MBR. В LBA 0 GPT-диска лежит фальшивый MBR с одним разделом типа 0xEE,
занимающим весь диск. Это не совместимость, а защита: старая утилита увидит «диск целиком
занят неизвестным разделом» и не станет его радостно переразмечать.
Backup GPT в конце диска. Красиво и надёжно ровно до момента, когда вы делаете
dd if=disk.img of=/dev/sdb на диск другого размера или расширяете виртуальный диск.
Зеркало оказывается не в конце — GPT считается повреждённой. Лечится одной командой:
$ sudo sgdisk -e /dev/sda # move backup GPT to end of disk
$ sudo partprobe /dev/sda
BIOS boot partition. На GPT-диске нет MBR gap — сразу за таблицей начинаются разделы.
Поэтому для BIOS-загрузки с GPT нужен отдельный крошечный раздел типа EF02 (1 МиБ), куда
GRUB положит core.img. Забыть про него — классическая ошибка при ручной разметке:
grub-install честно скажет embedding is not possible... GRUB can only be installed in this setup by using blocklists.
Отдельно стоит знать про Discoverable Partitions Specification — соглашение
systemd, по которому GUID типа раздела однозначно определяет точку монтирования.
Раздел с GUID 4f68bce3-… монтируется как / без всякого fstab, 933ac7e1-… — как /home.
Это то, на чём строятся современные иммутабельные дистрибутивы и образы для облака:
uapi-group.org/specifications/specs/discoverable_partitions_specification.
UEFI: прошивка как маленькая операционная система
UEFI (наследник Intel EFI из эпохи Itanium, ныне управляется UEFI Forum) устроен принципиально иначе. Это не «BIOS с мышкой», а платформенный фреймворк с собственным ABI, драйверной моделью и системой сервисов.
Что даёт UEFI загрузчику:
- Boot Services — доступны до вызова
ExitBootServices(): аллокация памяти, загрузка образов, работа с протоколами, файловый ввод-вывод, таймеры. Это среда исполнения. - Runtime Services — остаются доступны ядру после загрузки: чтение и запись NVRAM-переменных,
часы реального времени,
ResetSystem(). Linux использует их черезefivarfs. - Протоколы — интерфейсы в стиле COM:
SIMPLE_FILE_SYSTEM_PROTOCOL,GRAPHICS_OUTPUT_PROTOCOL(тот самый GOP, из которого получается фреймбуфер),LOADED_IMAGE_PROTOCOL.
Порядок загрузки живёт не на диске, а в NVRAM материнской платы:
$ efibootmgr -v
BootCurrent: 0000
Timeout: 1 seconds
BootOrder: 0000,0002,0001
Boot0000* ubuntu HD(1,GPT,8f2a...,0x800,0x100000)/File(\EFI\ubuntu\shimx64.efi)
Boot0001* UEFI: PXEv4 (MAC:001A2B3C4D5E) PciRoot(0x0)/Pci(0x1c,0x4)/MAC(001a2b3c4d5e,0)
Boot0002* Windows Boot Manager HD(1,GPT,8f2a...)/File(\EFI\Microsoft\Boot\bootmgfw.efi)
# Добавить свою запись руками
$ sudo efibootmgr --create --disk /dev/sda --part 1 \
--label "My Kernel" --loader '\EFI\custom\vmlinuz.efi' \
--unicode 'root=UUID=c219... ro quiet'
# Загрузиться ОДИН раз с другой записи (бесценно при отладке ядра по SSH)
$ sudo efibootmgr --bootnext 0002 && sudo reboot
Переменные видны как файлы:
$ ls /sys/firmware/efi/efivars/ | head -4
BootCurrent-8be4df61-93ca-11d2-aa0d-00e098032b8c
BootOrder-8be4df61-93ca-11d2-aa0d-00e098032b8c
SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c
dbx-d719b2cb-3d3a-4596-a3bc-dad00e67656f
# Первые 4 байта — атрибуты, дальше значение
$ od -An -t u1 /sys/firmware/efi/efivars/SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c
6 0 0 0 1
# ^ Secure Boot включён
Осторожно. Файлы в
efivarsимеют атрибут immutable и защищены не просто так: на некоторых прошивках (печально известный баг сrm -rf /на Samsung и MSI в 2016 году) удаление переменных превращает материнскую плату в кирпич. Никогда не делайте рекурсивных операций по/sys/firmware/efi/efivars/.
Проверить, в каком режиме загружена текущая система, — одна строка:
$ [ -d /sys/firmware/efi ] && echo "UEFI" || echo "Legacy BIOS"
Legacy BIOS
$ cat /sys/firmware/efi/fw_platform_size # 64 или 32 — разрядность прошивки, не ядра!
Разрядность прошивки — отдельная ловушка: существуют планшеты с 64-битным CPU и 32-битным
UEFI, где нужен bootia32.efi. И обратное правило: 32-битная прошивка не может запустить
64-битный EFI-образ, режимы должны совпадать.
Secure Boot и measured boot: две разные гарантии
Их постоянно путают, хотя они отвечают на разные вопросы.
Secure Boot отвечает на вопрос «можно ли это запускать?» и работает как политика: прошивка
проверяет цифровую подпись каждого исполняемого образа по базам ключей в NVRAM — PK
(Platform Key, владелец платформы), KEK (кто может обновлять базы), db (разрешённые
подписи и хэши), dbx (явно отозванные). Не прошёл проверку — не запустился.
Measured boot отвечает на вопрос «что именно было запущено?» и ничего не блокирует.
Каждый компонент перед передачей управления считает хэш следующего и «расширяет» им регистр
TPM: PCR[n] = SHA256(PCR[n] || hash). Операция необратима и не позволяет подделать
промежуточное состояние. Потом ключ шифрования диска можно запечатать (seal) на конкретные
значения PCR — TPM отдаст его только если система загрузилась ровно так же.
запрос пароля вручную
Почему в цепочке есть shim? Ключ Microsoft есть в db практически любой материнской
платы, а получить туда ключ каждого дистрибутива нереально. Поэтому Canonical, Red Hat,
SUSE подписывают у Microsoft маленькую программу-прокладку shim, а уже она проверяет
следующий компонент своим ключом или пользовательским из MOK (Machine Owner Key).
Практический сценарий, который встречает почти каждый: вы собрали out-of-tree модуль
(NVIDIA, VirtualBox, DKMS), и он не грузится с Required key not available.
# Создать собственный ключ и зарегистрировать в MOK
$ openssl req -new -x509 -newkey rsa:2048 -keyout MOK.priv -outform DER \
-out MOK.der -nodes -days 3650 -subj "/CN=my module signing key/"
$ sudo mokutil --import MOK.der # спросит пароль, применится при следующей перезагрузке
$ sudo /usr/src/linux-headers-$(uname -r)/scripts/sign-file sha256 MOK.priv MOK.der my_module.ko
$ mokutil --sb-state
SecureBoot enabled
Ещё один важный побочный эффект Secure Boot в Linux — lockdown mode. Ядро, загруженное с
включённым Secure Boot, ограничивает даже root: запрещены /dev/mem, kexec без подписи,
запись в MSR, некоторые kprobe. Это регулярно ломает профилировщики и отладчики; проверить
можно так:
$ cat /sys/kernel/security/lockdown
none [integrity] confidentiality
GRUB2: универсальный, монструозный, повсеместный
GRUB2 — не столько загрузчик, сколько миниатюрная ОС: у него свой шелл, скриптовый язык, модульная система (около 250 модулей), драйверы полутора десятков файловых систем, LVM, mdraid, LUKS, сети и даже поддержка графических тем.
Разберём реальный menuentry с нашей машины:
menuentry 'Ubuntu' --class ubuntu --class gnu-linux --class os $menuentry_id_option 'gnulinux-simple-082bd3af-…' {
recordfail
load_video
gfxmode $linux_gfx_mode
insmod gzio
insmod part_msdos
insmod ext2
set root='hd0,msdos1'
search --no-floppy --fs-uuid --set=root --hint-bios=hd0,msdos1 c21942c4-feaa-4713-98f0-32266838934b
linux /vmlinuz-6.8.0-136-generic root=/dev/mapper/vg68719-root ro
initrd /initrd.img-6.8.0-136-generic
}
Читается построчно как программа. insmod part_msdos и insmod ext2 — GRUB подгружает
собственные драйверы (модуль ext2 обслуживает и ext3/ext4). search --fs-uuid — вместо
хрупкого hd0,msdos1 находит раздел по UUID файловой системы: диски могут переименоваться
при добавлении контроллера, UUID — нет. linux загружает ядро и формирует boot_params,
initrd — грузит образ в память и записывает его адрес туда же.
Важнее всего понимать два уровня «root», которые постоянно путают:
set root='hd0,msdos1'— это корень для GRUB, раздел, откуда он читает файлы. У нас это отдельный/boot(/dev/sda1), поэтому путь к ядру —/vmlinuz-…, а не/boot/vmlinuz-….root=/dev/mapper/vg68719-rootв строкеlinux— параметр для ядра, где искать настоящую корневую ФС. Ядро само это устройство собрать не может (это LVM) — этим займётся initramfs.
Никогда не редактируйте /boot/grub/grub.cfg руками. Он генерируется:
# Что реально настраивают:
$ cat /etc/default/grub
GRUB_DEFAULT=0
GRUB_TIMEOUT=5
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"
GRUB_CMDLINE_LINUX=""
GRUB_DISABLE_OS_PROBER=false
# Скрипты-генераторы, выполняются по порядку номеров:
$ ls /etc/grub.d/
00_header 05_debian_theme 10_linux 20_linux_xen 30_os-prober 40_custom 41_custom
$ sudo update-grub # Debian/Ubuntu, обёртка над grub-mkconfig
$ sudo grub-mkconfig -o /boot/grub/grub.cfg # везде остальное
$ sudo grub2-mkconfig -o /boot/grub2/grub.cfg # RHEL/Fedora, другое имя каталога
Установка стадий на диск — отдельная операция, её забывают после замены диска:
$ sudo grub-install /dev/sda # BIOS: пишет boot.img в MBR, core.img в gap
$ sudo grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=ubuntu
Аварийный режим. Если система не грузится, в меню GRUB нажмите e — вы попадёте в
редактор строки запуска, изменения одноразовые. Или c — полноценная консоль:
grub> ls # (hd0) (hd0,msdos1) (hd0,msdos2)
grub> ls (hd0,msdos1)/ # посмотреть содержимое
grub> set root=(hd0,msdos1)
grub> linux /vmlinuz-6.8.0-136-generic root=/dev/mapper/vg68719-root ro
grub> initrd /initrd.img-6.8.0-136-generic
grub> boot
Этих пяти команд достаточно, чтобы поднять систему с испорченным grub.cfg. Стоит запомнить.
systemd-boot, UKI и минимализм
Против «GRUB как вторая ОС» есть противоположная философия: если UEFI уже умеет читать FAT32
и запускать PE-образы, зачем нужен большой загрузчик? Ядро Linux с 2011 года умеет собираться
как EFI stub — то есть vmlinuz сам является валидным EFI-приложением, и прошивка может
запустить его напрямую.
systemd-boot (бывший gummiboot) — это ~40 КиБ кода, единственная задача которых: показать меню и запустить выбранный образ. Никаких драйверов ФС — всё читается сервисами UEFI.
$ sudo bootctl install # ставит systemd-bootx64.efi и создаёт запись в NVRAM
$ bootctl status
System:
Firmware: UEFI 2.70 (American Megatrends 5.17)
Secure Boot: enabled (user)
Boot into FW: supported
Current Boot Loader:
Product: systemd-boot 255.4
# Конфигурация — плоские текстовые файлы, никакой генерации:
$ cat /boot/efi/loader/loader.conf
default arch.conf
timeout 3
console-mode max
$ cat /boot/efi/loader/entries/arch.conf
title Arch Linux
linux /vmlinuz-linux
initrd /intel-ucode.img
initrd /initramfs-linux.img
options root=UUID=b2c3... rw quiet
Формат loader/entries/*.conf — это Boot Loader Specification (BLS), общий стандарт;
его же понимают GRUB в Fedora и rEFInd. Смысл: добавление ядра — это создание текстового
файла, а не запуск генератора конфигов.
Unified Kernel Image (UKI) — следующий шаг и то, куда идёт индустрия. Ядро, initramfs, командная строка, микрокод и splash-картинка склеиваются в один PE-файл, который подписывается целиком:
$ ukify build --linux=/boot/vmlinuz-6.8.0 --initrd=/boot/initrd.img-6.8.0 \
--cmdline="root=PARTUUID=… ro quiet" \
--signtool=sbsign --secureboot-private-key=db.key --secureboot-certificate=db.crt \
--output=/boot/efi/EFI/Linux/linux-6.8.0.efi
# Секции внутри PE:
$ objdump -h linux-6.8.0.efi | grep -E '\.linux|\.initrd|\.cmdline|\.osrel'
.osrel .cmdline .initrd .linux .uname
Зачем это нужно на практике: при обычной схеме подписывается только ядро, а initramfs и cmdline не подписаны — атакующий с доступом к ESP может подменить initramfs и получить ключи от LUKS. UKI закрывает эту дыру: подпись покрывает всё сразу, и хэш измеряется в PCR11, на который удобно запечатывать ключи TPM. Это фундамент systemd-cryptenroll с TPM и иммутабельных систем вроде Fedora Silverblue и Flatcar.
Стоит знать и других игроков: rEFInd (удобный графический менеджер с автопоиском ОС),
Limine (популярен в хобби-разработке ОС), Petitboot (загрузчик на базе Linux+kexec
в POWER-серверах), iPXE (сетевая загрузка по HTTP/iSCSI), U-Boot (де-факто стандарт
в embedded ARM, со своим bootcmd и device tree).
Как загрузчик разговаривает с ядром Linux
Между «загрузчик прочитал файл» и «ядро запустилось» лежит документированный протокол: Documentation/arch/x86/boot.rst.
Файл bzImage — не просто сжатое ядро, а составной образ:
| Часть | Что это |
|---|---|
boot sector |
512 байт, при прямом запуске с дискеты печатают «use a boot loader» |
setup |
код реального режима, setup_header со смещения 0x1F1 |
payload |
сжатый vmlinux + декомпрессор arch/x86/boot/compressed/ |
setup_header — это ABI. Загрузчик заполняет структуру boot_params (ту самую «zero page»):
адрес и размер initrd (ramdisk_image, ramdisk_size), указатель на командную строку
(cmd_line_ptr), карту памяти от BIOS/UEFI (e820_table), параметры видеорежима. Дальше
прыгает в точку входа. Посмотреть заголовок можно инструментом из дерева ядра:
$ file /boot/vmlinuz-6.8.0-136-generic
Linux kernel x86 boot executable bzImage, version 6.8.0-136-generic (buildd@lcy02-amd64),
RO-rootFS, swap_dev 0XA, Normal VGA
$ extract-vmlinux /boot/vmlinuz-6.8.0-136-generic > /tmp/vmlinux # из scripts/ ядра
$ file /tmp/vmlinux
/tmp/vmlinux: ELF 64-bit LSB executable, x86-64, statically linked, BuildID[sha1]=…
Дальше внутри ядра порядок такой: код в arch/x86/boot/compressed/head_64.S включает
long mode, применяет KASLR (сдвигает ядро на случайный адрес), распаковывает vmlinux и
передаёт управление в start_kernel() (init/main.c). Оттуда — инициализация планировщика,
памяти, драйверов, и в конце rest_init() создаёт поток, который выполняет
kernel_init() → run_init_process(). Именно эта функция ищет /init:
/* init/main.c, упрощённо: порядок поиска первого пользовательского процесса */
if (ramdisk_execute_command) { /* обычно "/init" из initramfs */
ret = run_init_process(ramdisk_execute_command);
if (!ret) return 0;
}
if (execute_command) { /* если передали init=... в cmdline */
ret = run_init_process(execute_command);
if (!ret) return 0;
panic("Requested init %s failed (error %d).", execute_command, ret);
}
/* Дальше — исторический список кандидатов */
if (!try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh"))
return 0;
panic("No working init found.");
Эта функция объясняет два известных трюка: init=/bin/bash в cmdline даёт корневой шелл
без пароля (почему физический доступ к машине без шифрования диска = полный доступ), а
panic: No working init found означает, что корень смонтирован не тот или не смонтирован вообще.
initramfs: решение проблемы курицы и яйца
Ядро должно смонтировать корневую файловую систему. Но она может лежать на LVM поверх
mdraid поверх LUKS поверх NVMe, для которого нужен модуль драйвера, — а модули лежат в
/lib/modules, то есть на той самой ещё не смонтированной ФС.
Решение: загрузчик кладёт в память архив с минимальной пользовательской средой. Ядро
распаковывает его в tmpfs (это initramfs; старый initrd был именно образом
блочного устройства и работал иначе) и запускает оттуда /init. Тот делает всё сложное —
собирает RAID, спрашивает пароль LUKS, активирует LVM, ждёт появления устройств — и только
потом отдаёт управление настоящей системе.
Посмотрим на реальный образ:
$ ls -lh /boot/initrd.img-6.8.0-136-generic
-rw-r--r-- 1 root root 83M initrd.img-6.8.0-136-generic
$ lsinitramfs /boot/initrd.img-6.8.0-136-generic | wc -l
2139
$ sudo unmkinitramfs /boot/initrd.img-6.8.0-136-generic /tmp/ir && ls /tmp/ir
early early2 early3 main
Четыре секции: три несжатых early-cpio (микрокод AMD, микрокод Intel, ACPI-таблицы) и основной архив, сжатый zstd. 83 МиБ — это много, и вот почему:
$ cat /etc/initramfs-tools/initramfs.conf
MODULES=most # <- вот причина: "все драйверы дисков и ФС", а не только нужные
BUSYBOX=auto
COMPRESS=zstd
RUNSIZE=10%
FSTYPE=auto
MODULES=most — стратегия дистрибутива: образ должен грузиться на любом железе, потому что
диск могут переставить в другую машину. Для собственного образа под конкретную виртуалку
меняйте на MODULES=dep — размер падает до 10–15 МиБ, а время загрузки заметно сокращается
(меньше распаковывать и меньше modprobe). Расплата: перенесёте образ на другое железо — не загрузится.
Внутри main — привычная иерархия и скриптовый каркас:
$ ls /tmp/ir/main/scripts/
functions local nfs
init-top/ init-premount/ init-bottom/
local-top/ local-block/ local-premount/ local-bottom/
panic/
Жизненный цикл /init из initramfs-tools удобно смотреть как автомат состояний — именно в
эти точки вставляются хуки, и именно на них система обычно и виснет:
Финальный шаг заслуживает отдельного разбора, потому что вокруг него много путаницы:
# Примерно то, чем заканчивается /init:
mount -n -o move /sys ${rootmnt}/sys
mount -n -o move /proc ${rootmnt}/proc
mount -n -o move /dev ${rootmnt}/dev
exec switch_root ${rootmnt} ${init} "$@"
switch_root не является chroot. Он: (1) удаляет всё содержимое текущего rootfs,
освобождая память tmpfs, (2) делает mount --move нового корня в /, (3) chroot .,
(4) exec нового init — сохраняя PID 1. Отличие от pivot_root: тот сохраняет старый
корень доступным в подкаталоге и используется в контейнерах, тогда как switch_root
рассчитан именно на одноразовый initramfs и требует, чтобы вызывающий был PID 1 на rootfs.
Ключевое слово exec критично: без него шелл остался бы PID 1 и всё бы сломалось.
Отладка initramfs — самый полезный навык в этой статье
# Остановиться в конкретной фазе (initramfs-tools): добавить в cmdline GRUB
break=premount # также: top, modules, mount, mountroot, bottom, init
# Получите (initramfs) busybox-шелл ровно перед выбранным этапом:
(initramfs) cat /proc/cmdline
(initramfs) ls /dev/mapper/
(initramfs) lvm vgchange -ay
(initramfs) blkid
(initramfs) modprobe virtio_blk
(initramfs) exit # продолжить загрузку
# В dracut синтаксис другой:
rd.break=pre-mount rd.debug rd.udev.debug
# Убрать quiet, чтобы видеть всё:
# в GRUB нажать 'e', удалить "quiet splash", добавить:
# loglevel=7 console=ttyS0,115200 earlyprintk=serial systemd.log_level=debug
Три инструмента initramfs в дикой природе стоит различать:
| initramfs-tools | dracut | mkinitcpio | |
|---|---|---|---|
| Где | Debian, Ubuntu | RHEL, Fedora, SUSE, Gentoo | Arch |
| Конфиг | /etc/initramfs-tools/ |
/etc/dracut.conf.d/ |
/etc/mkinitcpio.conf |
| Расширение | shell-скрипты в hooks/, scripts/ |
модули в /usr/lib/dracut/modules.d/ |
HOOKS=(base udev …) |
| Внутри | busybox или klibc + свои скрипты | часто systemd в initramfs | busybox / systemd |
| Пересборка | update-initramfs -u -k all |
dracut -f --kver … |
mkinitcpio -P |
Отдельно отметьте строку про systemd: dracut по умолчанию запускает в initramfs полноценный
systemd со своими юнитами и .mount-зависимостями. Это даёт параллелизм и нормальные
таймауты вместо sleep в цикле, но переносит в раннюю фазу всю сложность юнит-системы.
Подробнее — в статье про системы инициализации.
Как это устроено в других семействах ОС
Различия здесь не косметические — они отражают разную философию.
FreeBSD. Классическая многостадийная схема, но идеологически иная: последняя стадия —
/boot/loader, полноценный интерпретатор Forth (FICL), а с FreeBSD 12 — альтернативно
Lua. Загрузчик умеет читать UFS и ZFS, включая boot environments — снимки корня, между
которыми можно переключаться прямо в меню загрузки. Настройка декларативна:
# /boot/loader.conf
zfs_load="YES"
vfs.root.mountfrom="zfs:zroot/ROOT/default"
kern.ipc.shmseg=1024
hw.vmm.topology.threads_per_core=2
# В работающей системе те же переменные видны через kenv(1):
$ kenv | grep smbios.system.product
smbios.system.product="VirtualBox"
Ключевое отличие от Linux: FreeBSD обычно не использует initramfs. Драйверы либо в ядре,
либо loader подгружает .ko-модули сам, ещё до старта ядра. Модель проще, но требует,
чтобы загрузчик знал вашу файловую систему.
OpenBSD. Демонстративный минимализм: biosboot (первая стадия) → /boot (вторая) → ядро.
Загрузчик читает FFS и понимает единственный конфиг /etc/boot.conf. Ничего похожего на
модули и скрипты. Зато OpenBSD раньше всех сделала обязательным KARL — Kernel Address
Randomized Link: при каждой загрузке ядро пересобирается из объектных файлов в случайном
порядке, так что бинарь физически уникален на каждой машине и после каждого reboot. Плюс
bsd.rd — ядро с встроенным ramdisk и установщиком, аналог rescue-режима.
NetBSD. Похожая двухстадийная схема, но с акцентом на переносимость: один и тот же
код bootxx адаптирован под десятки архитектур, а boot.cfg даёт меню. Уникальная черта —
rump kernels, позволяющие запускать драйверы NetBSD
как обычные пользовательские процессы, что сильно меняет представление о том, что должно
быть «в ядре при загрузке».
macOS и XNU. На Intel: UEFI → boot.efi → prelinkedkernel / kernelcache — ядро с уже
слинкованными kext’ами, чтобы не искать драйверы на диске. На Apple Silicon схема
принципиально другая и ближе к iOS: Boot ROM → LLB → iBoot → kernelcache, вся
цепочка проверяется Secure Enclave, а «уровень безопасности» выбирается в recoveryOS
(Full / Reduced / Permissive). Конфигурация — не текстовый файл, а NVRAM:
$ nvram boot-args # параметры ядра
$ sudo nvram boot-args="-v keepsyms=1" # -v = verbose boot вместо яблока
$ bputil -d # Apple Silicon: показать политику безопасности
Windows NT. UEFI → bootmgfw.efi (Boot Manager) → winload.efi → ntoskrnl.exe.
Конфигурация лежит в BCD — двоичном хранилище в формате куста реестра, редактируется
только утилитой:
bcdedit /enum # показать все записи
bcdedit /set {current} safeboot minimal # следующая загрузка в safe mode
bcdedit /set {current} testsigning on # разрешить неподписанные драйверы (для разработки)
bcdedit /timeout 5
Две особенности, о которые спотыкаются при dual-boot. Первая — Fast Startup: «выключение»
в Windows по умолчанию не выключение, а гибернация ядра в hiberfil.sys. Из-за этого NTFS
остаётся в «грязном» состоянии, и Linux монтирует раздел только для чтения либо портит его
при записи. Отключается: powercfg /h off. Вторая — ELAM (Early Launch Anti-Malware):
специальный драйвер антивируса стартует раньше всех прочих, что делает порядок инициализации
драйверов в NT более жёстко заданным, чем в Linux.
Сводная таблица:
| Linux | FreeBSD | OpenBSD | macOS (ARM) | Windows NT | |
|---|---|---|---|---|---|
| Загрузчик | GRUB2 / systemd-boot / EFI stub | /boot/loader (Forth/Lua) |
/boot |
iBoot | bootmgfw.efi |
| Конфиг | grub.cfg / BLS / NVRAM |
loader.conf |
boot.conf |
NVRAM | BCD (двоичный) |
| Ранний userspace | initramfs почти всегда | обычно нет | нет | нет | нет |
| Драйверы для корня | модули в initramfs | .ko грузит loader |
вкомпилированы | kernelcache | boot-критичные в реестре |
| Проверка подписи | Secure Boot + shim + MOK | опционально | нет по умолчанию | обязательна, Secure Enclave | Secure Boot обязателен |
Загрузка в облаке и виртуализации
Внутри виртуальной машины та же цепочка, но прошивка — программная: SeaBIOS для legacy, OVMF/edk2 для UEFI. Это открывает трюки, недоступные на железе.
Прямая загрузка ядра. QEMU умеет полностью пропустить загрузчик:
qemu-system-x86_64 -m 2G -enable-kvm \
-kernel /boot/vmlinuz-6.8.0 \
-initrd /boot/initrd.img-6.8.0 \
-append "root=/dev/vda1 console=ttyS0 rw" \
-drive file=disk.qcow2,if=virtio -nographic
QEMU сам заполняет boot_params и прыгает в ядро. Это стандарт для разработки ядра и для
микро-VM (Firecracker, Cloud Hypervisor вообще не имеют BIOS — только прямой boot, что даёт
время старта в 100–150 мс).
Сетевая загрузка. PXE: DHCP отдаёт next-server и имя файла, клиент тянет его по TFTP.
Современнее — HTTP Boot из UEFI 2.5 и iPXE, которые качают образ по HTTP(S) и умеют
скриптоваться. Диагностика начинается с dhcp-boot в конфиге и tcpdump -i eth0 port 67 or 69.
Конфигурация первой загрузки. cloud-init (читает метаданные с 169.254.169.254 или
с ISO с меткой cidata) и Ignition (Fedora CoreOS, работает в initramfs, до первого
запуска systemd — поэтому может переразметить диски). На нашей машине cloud-init виден прямо
в критической цепочке загрузки:
$ systemd-analyze critical-chain | head -12
graphical.target @7.284s
└─multi-user.target @7.284s
└─mysql.service @3.299s +1.307s
└─network.target @3.277s
└─NetworkManager.service @2.478s +798ms
└─dbus.service @2.379s +80ms
└─basic.target @2.370s
└─sysinit.target @2.326s
└─cloud-init-local.service @1.876s +448ms
└─systemd-remount-fs.service @397ms +46ms
kexec — загрузка без прошивки вообще. Ядро может загрузить другое ядро напрямую, минуя BIOS/UEFI, POST и весь discovery. Экономит десятки секунд на серверах с долгой инициализацией RAID-контроллеров:
$ sudo kexec -l /boot/vmlinuz-6.8.0-136-generic \
--initrd=/boot/initrd.img-6.8.0-136-generic \
--reuse-cmdline
$ sudo systemctl kexec # корректно остановить сервисы и прыгнуть
На этом же механизме построен kdump: заранее загруженное «аварийное» ядро в
зарезервированной памяти (crashkernel=512M в cmdline), которое стартует при панике и
сохраняет дамп предыдущего.
Типичные заблуждения
«UEFI требует GPT, BIOS требует MBR». Неверно в обе стороны. UEFI по спецификации
обязан поддерживать и MBR-диски; загрузиться в BIOS-режиме с GPT тоже можно — нужен лишь
раздел EF02. Связка популярна, но не обязательна. А вот Windows действительно
требует GPT для UEFI-загрузки — это ограничение Windows, а не UEFI.
«Secure Boot защищает от вирусов». Он проверяет только цепочку до старта ядра. Malware в userspace, вредоносный контейнер, скомпрометированная зависимость в вашем npm-пакете — всё это Secure Boot не видит вообще.
«initrd и initramfs — одно и то же». Разные механизмы: initrd — образ блочного
устройства, монтируется через /dev/ram0, требует драйвер ФС в ядре и использует
pivot_root. initramfs — cpio-архив, распаковывается в tmpfs, использует switch_root,
память освобождается полностью. Файл называется initrd.img по историческим причинам —
внутри почти всегда initramfs.
«Ядро монтирует корень само». С initramfs — нет. Корень монтирует /init в
пользовательском пространстве. Ядро умеет монтировать корень напрямую (root=/dev/sda1
без initrd), но только если драйвер и ФС вкомпилированы, что в дистрибутивных ядрах не так.
«/boot/grub/grub.cfg — конфиг, который надо править». Он генерируется и будет затёрт
следующим update-grub — например, при установке обновления ядра. Правьте
/etc/default/grub и /etc/grub.d/40_custom.
«systemd-analyze blame показывает, что тормозит загрузку». Не совсем: он показывает
длительность юнитов, включая те, что стартовали параллельно и ни на что не влияли. На нашей
машине первая строка — snapd.service с 11 часами, потому что это долгоживущий сервис.
Смотреть надо critical-chain, где видна именно цепочка зависимостей.
«Медленная загрузка — вина ядра». Реальный вывод той же машины:
$ systemd-analyze
Startup finished in 5.156s (kernel) + 7.743s (userspace) = 12.899s
graphical.target reached after 7.284s in userspace.
Ядро — 5 секунд, из которых бóльшая часть обычно уходит на распаковку 83-мегабайтного initramfs и probe устройств. Userspace — почти 8. Оптимизировать чаще всего нужно второе.
Практика: что делать, когда не грузится
Диагностический алгоритм, который стоит держать в голове:
- Экран пустой, никакого меню. Проблема до загрузчика: не найден загрузочный носитель,
не та запись в BootOrder, испорчен MBR/core.img. Загрузитесь с LiveUSB, проверьте
efibootmgr -vили переустановитеgrub-install. grub rescue>вместо меню. core.img жив, но не нашёл/boot/grub. Ищем раздел:ls,set prefix=(hd0,msdos1)/grub,set root=(hd0,msdos1),insmod normal,normal.- Меню есть, ядро не стартует. Уберите
quiet splash, добавьтеconsole=ttyS0,115200, смотрите, где обрывается вывод. Kernel panic: VFS: Unable to mount root fs. Ядро загрузилось, корень не нашёлся. Почти всегда — нет драйвера в initramfs или неверныйroot=.- Приглашение
(initramfs). Вы уже в userspace.blkid,lvm vgchange -ay,cat /proc/cmdline— обычно всё видно за минуту. Emergency mode/you are in emergency shell. Корень смонтирован, systemd не смог выполнитьfstabили юнит.journalctl -xb,systemctl --failed. Частая причина — строка в/etc/fstabдля несуществующего устройства без опцииnofail.
Универсальный ремонтный рецепт из LiveUSB:
$ sudo mount /dev/mapper/vg-root /mnt
$ sudo mount /dev/sda1 /mnt/boot
$ sudo mount /dev/sda2 /mnt/boot/efi # если UEFI
$ for d in dev proc sys run; do sudo mount --bind /$d /mnt/$d; done
$ sudo chroot /mnt
# grub-install /dev/sda && update-grub && update-initramfs -u -k all
И небольшой набор для сокращения времени загрузки, проверенный на облачных образах:
# 1. Урезать initramfs под конкретное железо
$ sudo sed -i 's/^MODULES=most/MODULES=dep/' /etc/initramfs-tools/initramfs.conf
$ sudo update-initramfs -u -k all # 83M → ~12M
# 2. Убрать таймаут меню GRUB (в облаке меню всё равно никто не видит)
$ sudo sed -i 's/^GRUB_TIMEOUT=.*/GRUB_TIMEOUT=0/' /etc/default/grub
$ echo 'GRUB_RECORDFAIL_TIMEOUT=0' | sudo tee -a /etc/default/grub
$ sudo update-grub
# 3. Найти реальных виновников в userspace
$ systemd-analyze critical-chain
$ systemd-analyze plot > boot.svg # наглядная диаграмма Ганта всей загрузки
$ sudo systemctl disable NetworkManager-wait-online.service # частый виновник +5–15 с
Мини-итог
- Загрузка — это цепочка передач управления, где каждый уровень строит абстракции для следующего: сектор → загрузчик → ядро → userspace.
- Legacy BIOS не умеет ничего, кроме чтения сектора, поэтому вся сложность в многостадийном загрузчике; UEFI — платформа с драйверами, поэтому загрузчик может быть тривиальным или отсутствовать вовсе (EFI stub, UKI).
- MBR против GPT — это не только 2 ТиБ: GPT даёт CRC, зеркало метаданных, GUID типов и Discoverable Partitions, на которых строится современная иммутабельная инфраструктура.
- Secure Boot отвечает «можно ли запускать», measured boot — «что именно было запущено». Второе гораздо интереснее для автоматической расшифровки дисков через TPM.
- initramfs существует ровно затем, чтобы разорвать циклическую зависимость «драйвер корня
лежит на корне». Ключевые навыки —
break=,lsinitramfs,unmkinitramfs. - BSD, macOS и Windows решают те же задачи иначе: FreeBSD кладёт логику в Forth/Lua-загрузчик и обходится без initramfs, OpenBSD — в минимализм и KARL, Apple — в аппаратную цепочку доверия, Microsoft — в двоичный BCD и жёсткий порядок драйверов.
- Измеряйте прежде чем оптимизировать:
systemd-analyze critical-chainиsystemd-analyze plot.
Источники и что читать дальше
- UEFI Specification — первоисточник по GPT, ESP, переменным и протоколам: uefi.org/specifications
- Linux kernel: The Linux/x86 Boot Protocol — точный контракт между загрузчиком и ядром: kernel.org/doc/html/latest/arch/x86/boot.html
- Documentation/filesystems/ramfs-rootfs-initramfs.rst — авторский текст Роба Лэндли о разнице initrd и initramfs: kernel.org/doc/html/latest/filesystems/ramfs-rootfs-initramfs.html
- GNU GRUB Manual — модули, скриптовый язык, восстановление: gnu.org/software/grub/manual/grub/
- Boot Loader Specification и Discoverable Partitions Specification от UAPI Group: uapi-group.org/specifications
man 7 bootup,man bootctl,man systemd-analyze,man dracut.cmdline,man 8 efibootmgr,man 8 switch_root- FreeBSD Handbook, глава «The FreeBSD Booting Process»: docs.freebsd.org/en/books/handbook/boot/
- Apple Platform Security — цепочка доверия на Apple Silicon: support.apple.com/guide/security/
- Windows Internals, 7th ed., Part 2, Russinovich, Solomon, Ionescu — глава про startup and shutdown, лучшее описание BCD и winload
- OSDev Wiki — практический источник для тех, кто пишет загрузчик сам: wiki.osdev.org/Bootloader
- coreboot — если интересно, что можно убрать из прошивки: coreboot.org
Что дальше
Мы довели систему до момента, когда ядро выполняет exec первого настоящего
пользовательского процесса. Всё, что происходит дальше — параллельный запуск сотен сервисов,
управление зависимостями, сокет-активация, перезапуск упавших демонов, — это работа системы
инициализации, и подходы здесь различаются радикально: от 200 строк shell-скриптов в runit
до многокомпонентного systemd и rc.d во FreeBSD.
Системы инициализации: SysV, systemd, runit, OpenRC, launchd, rc.d в BSD