На этой лекции будет рассмотрен процесс загрузки ОС. Будет рассмотрена схема работы загрузчика BIOS, а также обсуждена работа UEFI, GPT и GRUB
Быстрый поиск
Задача загрузки
Загрузка ОС — это многоуровневый процесс передачи управления от железа к ОС. На выключенном устройстве все данные хранятся на дисках, при этом в момент включения компьютера доступ ко всем неинтегрированным компонентам платы (на которую и приходит сигнал включения) отсутствует. В рамках загрузки необходимо провести следующие действия:
- Инициализация. Первое, что необходимо сделать при включении устройства это привести в рабочее состояние процессор, память, контроллеры дисков. Код для поиска доступных к инициализации компонентов и активации основных элементов управления хранится в энергонезависимой памяти (NVRAM) на материнской плате и называется прошивка. При запуске процессор начинает исполнение со строго определённого адреса этой памяти, где находится первая инструкция прошивки.
- Поиск устройств. После активации основных компонентов прошивка сканирует доступные устройства: жёсткие диски, USB-флешки, сетевые интерфейсы. На каждом производится поиск загружаемых образов для продолжения загрузки. После просмотра всех вариантов собирается список доступных методов загрузки. На основе выбранной конфигурации определяется приоритет автоматической попытки загрузки.
- Загрузка загрузчика. После выбора источника загружается непосредственно загрузчик. Это промежуточная программа, более функциональная, чем прошивка. Она занимается определением доступных загрузочных разделов источника, проверкой корректности загрузочных записей, непосредственно запуском загрузки ОС с определёнными параметрами и передачей управления уже ей.
Для корректной загрузки системы важный все этапы. И если первые два прибиваются практически гвоздями к конкретному оборудованию (на деле память прошивки уже давно перестала быть неперезаписываемой, хоть и была такой в начале своего появления; однако при классическом использовании устройства без влезаний с отвёрткой внутрь с самой прошивкой вы просто так не встретитесь), третий, а именно сама работа загрузчика, претерпел большие изменения.
BIOS и MBR
Чтобы понять, почему нужен был UEFI, нужно разобраться в BIOS. Сейчас эта аббревиатура (Basic Input/Output System) стала именем нарицательным для всех загрузчиков, однако изначально BIOS был разработкой компании IBM, представленной в 1981 году. Принцип BIOS был поразительно прост: он читал сектор диска с таблицей разделов, писал его в оперативную память, проверял валидность и передавал ему управление. Загрузчик с сектора просматривал таблицу, находил первую же валидную загрузочную запись, после чего также просто загружал её и передавал управление. И уже в самом загрузочном разделе должен был содержаться полноценный загрузчик, который должен был заниматься поиском ядра, его загрузкой, чтением ФС и так далее.
За простотой системы скрывалось столько legacy-решений (legacy это костыли реализаций чего-либо, которые из-за долгих перепетий, медленного развития или банального нежелания или невозможности изменения стали условными стандартами, под которые приходилось и приходится приспосабливать новые разработки “для совместимости”), что полноценное развитие BIOS в его изначальном варианте было даже потенциально невозможно. Пройдёмся лишь по самым значимым таким решениям:
- Поскольку BIOS писался под IBM-устройства, он был строго привязан под их архитектуру. Для систем не с x86, а с ARM, MIPS и другими возможности работать с BIOS не было. Это порождало множество одинаковых по своей логике решений для каждой отдельной платформы.
- Работа с BIOS накладывала жёсткие ограничения на диски хранения загрузочных данных. Во-первых, таблица разделов, которую читал BIOS, должна была находиться строго в первом секторе диска. BIOS просто читал только его. И не найдя таблицы выдавал ошибку. Во-вторых, размер сектора должен был быть строго 512 байт, чтобы уместить таблицу и MBR-загрузчик с одной стороны и чтобы по нему оставалась возможность прямо адресоваться с другой.
- В работе BIOS было использовано огромное количество «магических чисел». Так, например, выполнение кода загрузки было строго фиксированным адресом в памяти:
0x7C00. Сам BIOS загружался на это место, а когда считывал загрузочный раздел, перегружал себя в другое место памяти, загружал на этот адрес MBR-загрузчик и передавал туда управление. Что не удивительно, этот загрузчик делал то же самое, но уже с данными из записи раздела. Ещё одно магическое число возникало при проверке валидности загрузочного раздела и загрузочных записей. Их корректность определялась просто наличием двух байт0x55ААв конце. При нарушении этих бит при всей правильности остальных данных загрузка не продолжалась.
Сама структура раздела с таблицей — Master Boot Record — также была жёстко ограниченной. Сам сектор был структурирован так:

- 446 байт кода загрузчика
- 64 байта таблицы разделов строго на 4 записи
- 2 байта сигнатуры (0x55AA)
Перечислим здесь основные legacy-решения:
- Во-первых, жёсткое ограничение на количество записей. Поскольку сектор был фиксированного размера, большее количество просто нельзя было в него впихнуть. для увеличения количества записей применялись связные описания: в таблице первые две записи были описаниями разделов, а третья запись описывала местоположение следующего сектора, где лежит продолжение таблицы. Загрузчик считывал её, получал следующее звено таблицы и смотрел её записи. И так пока не просмотрит весь список.
- Поскольку запись MBR-таблицы могла адресовать только 32-битные адреса, для систем с MBR был физический предел диска в 2 ТБ. Больше они просто не могли адресовать.
- MBR никак не защищался, не поддерживал резервных копий. Любой сбой или повреждение сектора вели к невозможности автовосстановления таблицы разделов.
UEFI как современная модель загрузки
К началу 2000-х Intel и другие компании разработали новый унифицированный формат загрузчика — UEFI (Unified Extensible Firmware Interface).
UEFI проектировалась, как архитектурно-независимая система (все объекты в ней читаются загрузчиком как файлы, а не как прямой исполняемый код в случае BIOS), в которой объекты выгружаются интерпретируются и запускаются унифицированным способом (все такие объекты называются EFI-приложениями). Для работы EFI содержит все необходимые сервисы и подсистемы загрузки. Для описания настроек используются специализированные параметры, записываемые в NVRAM для сохранения параметров при выключении устройства. Данные параметры называются Boot Variables, через них организован простейший интерфейс управления загрузкой. С этими параметрами можно работать с помощью утилиты efibootmgr. Основных загрузочных переменных немного:
[papillon_rouge@BaseALT-Papillon ~]$ efibootmgr
BootCurrent: 0000
Timeout: 0 seconds
BootOrder: 0000,0005,0003,0004,0006,0007,0001,0002
Boot0000* ALT Linux HD(1,GPT,603203f2-cb05-9547-a825-644ead6d4cb0,0x800,0xff800)/\EFI\altlinux\shimx64.efi0400000049535048
Boot0001* Wi-Fi IPV4 Network PciRoot(0x0)/Pci(0x14,0x3)/MAC(4c5f70e779fd,1)/Wi-Fi(00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00)/IPv4(0.0.0.0,0,DHCP,0.0.0.0,0.0.0.0,0.0.0.0)4eac0881119f594d850ee21a522c59b20000000049535048
Boot0002* Wi-Fi IPV6 Network PciRoot(0x0)/Pci(0x14,0x3)/MAC(4c5f70e779fd,1)/Wi-Fi(00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00)/IPv6([::],0,Static,[::],[::],64)4eac0881119f594d850ee21a522c59b20000000049535048
Boot0003* IPV4 Network - Realtek PCIe GBE Family Controller PciRoot(0x0)/Pci(0x1c,0x0)/Pci(0x0,0x0)/MAC(2c58b9589ec7,0)/IPv4(0.0.0.0,0,DHCP,0.0.0.0,0.0.0.0,0.0.0.0)4eac0881119f594d850ee21a522c59b20000000049535048
Boot0004* IPV6 Network - Realtek PCIe GBE Family Controller PciRoot(0x0)/Pci(0x1c,0x0)/Pci(0x0,0x0)/MAC(2c58b9589ec7,0)/IPv6([::],0,Static,[::],[::],64)4eac0881119f594d850ee21a522c59b20000000049535048
Boot0005 USB: PciRoot(0x0)/Pci(0x14,0x0)4eac0881119f594d850ee21a522c59b20b80010049535048
Boot0006 USB NETWORK BOOT: PciRoot(0x0)/Pci(0x0,0x0)/IPv4(0.0.0.0,0,DHCP,0.0.0.0,0.0.0.0,0.0.0.0)4eac0881119f594d850ee21a522c59b21b08020049535048
Boot0007 USB NETWORK BOOT: PciRoot(0x0)/Pci(0x0,0x0)/IPv6([::],0,Static,[::],[::],0)4eac0881119f594d850ee21a522c59b21b10020049535048
[papillon_rouge@BaseALT-Papillon ~]$
Boot####— переменные описания конкретной загрузочной записи. через них описываются как записи из таблицы разделов, так и записи о возможной загрузке по сети или с внешних носителей;BootOrder— Порядок проверки и загрузки записей. В отличие от BIOS, где в случае невалидности меток записи загрузка ломалась, в EFI записи полноценно проверяются на корректность. В случае невозможности работы с конкретной записью EFI просто переходит к следующей. Потенциально в этот список можно добавить вообще любые записи, и ситема продолжит работать.BootNext— при описании ключа-nможно задать однократный первый приоритет загрузки. При следующей активации EFI она в первую очередь будет загружать указанную запись.BootCurrent— Переменная описания загруженной на данный момент записи (той, которой принадлежит ваша сессия использования устройства)Timeout— Задержка перед автоматическим выбором записи при загрузке.
GUID Partition Table
На замену старой таблицы записей MBR также пришла новая — GPT. Она поддерживала совместимый интерфейс со старыми системами, ориентированными на MBR, при этом позволяла описывать большее количество разделов и большие объёмы памяти.
Для каждого раздела генерировался уникальный 128-битный идентификатор GUID для отслеживания разделов и быстрого поиска необходимых данных. Сами записи в таблице из 16-байтных стали 128-байтными, описывали дополнительные параметры конкретной загрузки. По умолчанию таблица содержала 128 записей. Однако GPT была сконструирована так, чтобы иметь возможности расширения таблицы без ограничений. В GPT изначально были предусмотрены системы контроля целостности и резервного копирования таблицы: CRC32-коды для проверки заголовка и записей таблицы, дублирование заголовка в конце таблицы, резервная копия массива записей в конце диска.
Структура GPT на диске

LBA 0 Protective MBR (для совместимости со старыми программами)
LBA 1 Primary GPT Header
LBA 2-33 Partition Entry Array (128 записей о разделах)
[содержимое разделов]
LBA -32..−1 Secondary Partition Entry Array (резервная копия)
LBA -1 Secondary GPT Header (резервный заголовок)
- Protective MBR — специальный MBR-совместимый формат заголовка, защищающий от повреждения основные GPT-разделы
- Primary GPT Header — заголовок таблицы. Содержит сигнатуру “EFI PART”, версию, размеры, CRC32 целостности, адреса основного и резервного заголовков.
- Partition Entry Array — непосредственно массив записей о разделах. Каждая запись содержит тип раздела (GUID), уникальный GUID раздела, начальный и конечный LBA, атрибуты, имя раздела.
- Резервные копии в конце диска
EFI System Partition
Несмотря на универсальность модели описания данных в GPT использовать её для совершенно случайно размеченных дисков всё равно было бы затруднительно. Как минимум, для построения таблицы требовалось бы полное сканирование диска для поиска всех загрузочных данных и описания записей по ним. Поэтому для хранения и работы с EFI-приложениями используется специальный раздел — ESP. Он не привязан к конкретной ОС, наоборот, он является единой точкой хранения всех доступных на носителе загрузочных объектов. Структура его ФС фиксирована (/boot/efi — это точка монтирования раздела ESP к нашей ФС; про точки монтирования подробнее будет рассказано в соответствующей лекции):
[root@BaseALT-Papillon ~]# tree /boot/efi
/boot/efi
└── EFI
├── altlinux
│ ├── BOOTX64.CSV
│ ├── grub.cfg
│ ├── grubx64.efi
│ ├── mmx64.efi
│ └── shimx64.efi
└── BOOT
├── BOOTX64.EFI
├── fbx64.efi
└── mmx64.efi
4 directories, 8 files
[root@BaseALT-Papillon ~]#

Сама ФС по стандарту EFI — FAT32. Это простая ФС с рекурсивным описанием зависимых блоков, поддерживаемая практически любой системой. Основная директория EFI содержит поддиректорию BOOT хранения основного загрузчика, а также поддиректории конкретных установленных ОС с их загрузчиками и дополнительными системами. Объектами на ESP могут быть любые загрузочные исполняемые файлы.
Многофункциональный загрузчик GRUB
Хотя UEFI и может запустить ядро напрямую, обычно между ним и ядром находится отдельный загрузчик — GRUB (он — не единственный в своём роде, но поскольку он является самым популярным, рассматривать будем его).
GRUB позволяет задать более сложную схему загрузки:
- Описать конкретную версию ядра, если у вас их несколько
- Передать специальные параметры загрузки
- Настроить работу дополнительных утилит (например, для дешифровки корневой ФС)
Современный GRUB поддерживает работу с обоими первичными загрузчиками (BIOS / UEFI), а также может запусктаь сторонние загрузчики других ОС.
Настройка GRUB осуществляется через описание параметров в /etc/default/grub и отдельных сценариев в /etc/grub.d/.
[root@BaseALT-Papillon ~]# cat /etc/default/grub
GRUB_SYSCONFIG_VERSION=1
GRUB_AUTOUPDATE_CFG=true
GRUB_AUTOUPDATE_CFGNAME=/boot/grub/grub.cfg
GRUB_TOP_LEVEL=/boot/vmlinuz
GRUB_VMLINUZ_SYMLINKS=yes
GRUB_DISABLE_RECOVERY=false
GRUB_CMDLINE_LINUX_DEFAULT=' resume=/dev/disk/by-uuid/32d7db08-b2f0-4f5f-9465-ae598d68e3ba panic=30 quiet splash kvm.enable_virt_at_load=0 psi=1 intel_pstate=active intel_pstate=per_cpu_perf_limits'
GRUB_CMDLINE_LINUX=''
GRUB_CMDLINE_LINUX_RECOVERY='failsafe vga=normal kvm.enable_virt_at_load=0'
GRUB_TERMINAL_OUTPUT='gfxterm'
GRUB_GFXMODE='auto'
GRUB_DEFAULT='saved'
GRUB_SAVEDEFAULT=true
GRUB_COLOR_NORMAL=light-gray/black
GRUB_COLOR_HIGHLIGHT=white/dark-gray
GRUB_DISTRIBUTOR="ALT Linux"
GRUB_BOOTLOADER_ID="altlinux"
[root@BaseALT-Papillon ~]#
После описания настроек необходимо сконфигурировать новый EFI-образ с помощью утилиты update-grub для применения изменений.
Изменения можно вносить и на лету: для этого при активации GRUB необходимо выбрать e для открытия меню GRUB и ввода однократно применяемых настроек.
Возможность обойтись без GRUB
UEFI может запустить ядро напрямую, без GRUB. Данный механизм называется EFI stub. Если ядро Linux скомпилировано с опцией CONFIG_EFI_STUB, оно получает PE-заголовок и выглядит как EFI-приложение. UEFI может его загрузить напрямую, как любое другое EFI-приложение.
При таком способе загрузки цепочка загрузки получается меньше и быстрее, а аткже требует меньше зависимостей. Однако теряется возможность управления несколькими версиями ядра и усложняется процесс передачи параметров ядра. Кроме этого теряется возможность мультизагрузки.