Когда мы включаем компьютер, он успевает совершить несколько этапов работы ещё до того, как загрузится операционная система. В этом посте будет рассмотрено, как загружается типичный процессор с архитектурой x86. Это очень сложный и многоступенчатый процесс. Здесь его структура будет представлена только в самом общем виде. От загрузочной прошивки зависит, каким именно путём процессор придёт к тому состоянию, в котором сможет загрузить операционную систему. Мы проследим этот процесс на примере опенсорсной загрузочной прошивки coreboot.
❯ До того, как будет подано питание
Начнём с чипа BIOS, также именуемого загрузочным ROM. Чип BIOS – это кремниевый элемент на материнской плате компьютера, в нём хранится информация (в байтах). Нас интересуют две составляющие его работы. Во-первых, (как минимум, отчасти) он отображается в памяти на адресное пространство ЦП – и это значит, что ЦП может обращаться к нему точно как к ОЗУ. В частности, ЦП может направить свой указатель инструкций на код, выполняемый внутри чипа BIOS. Во-вторых, те байты, что хранятся в чипе BIOS, соответствуют самым первым инструкциям, выполняемым в ЦП. Также в чипе BIOS содержатся и другие фрагменты кода и данных. В типичном BIOS находится флеш-дескриптор (таблица с содержимым чипа BIOS), регион BIOS (первые инструкции, которые должны быть выполнены), Intel ME (движок управления Intel) и GbE (gigabit ethernet). Как видите, чип BIOS совместно используется несколькими компонентами системы, а не только ЦП.
❯ Когда подано питание
Современные чипы Intel оснащаются так называемым Intel Management Engine. Как только получено питание (от батареи или электросети), включается Intel ME. Он выполняет свой набор процедур инициализации, для чего требуется прочитать во флеш-дескрипторе BIOS, где именно находится регион Intel ME, а затем, именно из этого региона BIOS прочитать код и конфигурационные данные. Далее мы нажимаем кнопку питания на системнике, и в дело вступает ЦП. В многопроцессорной системе всегда есть выделенный процессор BSP (начальный процессор), применяемый именно для этой цели. Как бы то ни было, процессор всегда переходит в так называемый 16-разрядный реальный режим, где указатель инструкций направлен на адрес 0xffff.fff0. Это вектор сброса.
Может возникнуть вопрос: как 16-разрядный системный адрес 0xffff.fff0, явно находящийся за пределами 0xffff, максимального 16-разрядного значения? В 16-разрядном режиме физический адрес рассчитывается так: на 4 разряда влево сдвигается сегментный регистр кода (CS), а затем добавляется адрес указателя инструкций (IP). При сбросе в IP содержится значение 0xfff0, а в CS – значение 0xf000. По вышеприведённой формуле вычисляем, что физический адрес должен быть:
CS << 4 + IP = 0x000f.0000 + 0xfff0 = 0x000f.fff0
— всё равно не то, что мы ожидали. Дело в том, что при сбросе система находится в «особом» реальном режиме, где первые 12 адресных строк всегда известны заранее. Поэтому все адреса имеют вид 0xfffx.xxxx. В нашем случае это означает, что мы должны сами установить 12 наиболее важных разрядов в выведенном нами адресе, что и даёт в результате ожидаемый адрес 0xffff.fff0. Эти 12 строк в адресе так и остаются «утверждёнными», пока не будет выполнен длинный джамп. После этого они выходят из состояния assert, и возобновляются вычисления, характерные для адресации в реальном режиме.
Кроме того, чип BIOS настраивается таким образом, что первая инструкция из BIOS, которая должна быть выполнена, находится в процессоре по адресу 0xffff.fff0. Следовательно, процессор способен выполнить первую инструкцию из региона BIOS в чипе BIOS. В этом регионе содержится так называемая загрузочная прошивка (boot firmware). Примеры загрузочной прошивки – это различные реализации UEFI, coreboot и классический BIOS.
В самом начале работы загрузочная прошивка переключается в 32-разрядный режим. Этот режим также является «защищённым» — то есть, включается сегментирование, и различными сегментами адресного пространства в процессоре можно управлять с разными правами доступа. Но в загрузочной прошивке будет всего один сегмент, поэтому сегментация, фактически, отключается. Такая ситуация называется «плоским режимом».
❯ Ранние инициализации
Стоит отметить, что на данном этапе загрузочного процесса у нас нет доступа к DRAM (динамической памяти с произвольным доступом). Инициализировать DRAM – одна из основных целей загрузочной прошивки. Но перед инициализацией DRAM необходима некоторая подготовительная работа.
Вставки микрокода – это своего рода патчи, обеспечивающие корректную работу ЦП. Intel продолжает публиковать микрокодовые патчи для различных ЦП. Загрузочная прошивка задействует эти патчи на очень раннем этапе процесса загрузки. В этом процессе участвует, в том числе, так называемый южный мост, или контроллер ввода-вывода (ICH), или мост контроллера периферии (PCH). Есть и такие операции инициализации, которые должны выполняться специально для ICH. Например, в состав ICH может входить сторожевой таймер, начинающий отсчёт в момент инициализации DRAM. Этот сторожевой таймер нужно отключить первым.
Разумеется, все эти операции выполняет прошивка, код для которой кто-то должен написать. В современном коде, как правило, используется стек. Но, как было сказано выше, DRAM пока не инициализирована, поэтому у нас в распоряжении нет памяти. Как же написать и выполнить этот код? Нужно работать с кодом без стека. Он или пишется вручную на ассемблере x86, или, как в случае с coreboot, пишется на C, а затем собирается при помощи специального компилятора ROMCC, который преобразует команды C в бесстековые ассемблерные инструкции. Конечно же, при этом налагаются определённые ограничения на код, скомпилированный при помощи ROMCC, и любой код так выполнять не получится. То есть, стек нам нужен как можно скорее.
На следующем шаге необходимо оборудовать так называемый «кэш в роли оперативной памяти» (CAR). Как правило, загрузочная прошивка устанавливает кэши ЦП таким образом, что их можно временно использовать в качестве ОЗУ. Именно так прошивке удаётся выполнять код, который не является бесстековым, но в нём действуют ограничения по части размеров стека и общего объёма доступной памяти.
❯ Инициализация памяти и пакет Intel FSP
В системах Intel за выполнение инициализации отвечает большой двоичный объект (блоб) под названием Intel Firmware Support Package (FSP). Он предоставляется Intel в двоичной форме. Intel FSP выполняет массу сложной работы, касающейся начальной загрузки процессоров Intel, а не только занимается инициализацией памяти. В принципе, это трёхступенчатый API. Способ взаимодействия загрузочной прошивки с FSP задаётся в виде нескольких параметров и адреса возврата, после чего выполняется переход на стадию FSP. Стадия FSP выполняется с учётом выстьавленных параметров, а потом по адресу возврата выполняется переход обратно в загрузочную прошивку. Это продолжается на протяжении трёх стадий работы с FSP, в следующем порядке:
- TempRamInit(): здесь частично выполняется инициализация ОЗУ, после чего управление передаётся обратно загрузочной прошивке. Загрузочная прошивка может стронуть несколько действий, а затем перейти к следующей стадии. Дело в том, что на следующем шаге инициализируются чипсет и память, а на это может потребоваться немало времени. Например, на тренировку памяти нужно много времени. Поэтому здесь удобно сделать так, чтобы загрузочная прошивка запустила инициализацию других процессов, например, раскрутила жёсткий диск, которому может потребоваться некоторое время, чтобы стабилизироваться.
- FspInitEntry(): Именно здесь мы фактически добираемся до. Также здесь выполняются операции по инициализации других аппаратных компонентов, в частности, PCH и самого ЦП. По завершении этого этапа управление передаётся обратно загрузочной прошивки. Но с этого момента память уже инициализирована, и передача управления и данных происходит иначе, нежели на этапе TempRamInit. После этого этапа прошивка выполняет большинство оставшихся инициализаций – которые описаны в следующем разделе – а потом передаёт управление на следующую стадию FSP.
- NotifyPhase(): Именно на данном этапе загрузочная прошивка передаёт управление обратно FSP и устанавливает параметры, сообщающие FSP, какие действия нужно совершить, прежде, чем свернуть работу. Те вещи, которые FSP может здесь сделать, платформозависимые, но здесь, например, перечисление PCI-устройств.
❯ После инициализации памяти
Как только DRAM готова, она даёт новый импульс загрузочному процессу. Первое, что делает прошивка – копирует себя в DRAM. Это делается при помощи “memory aliasing” (совмещения страниц в памяти). Это означает, что операции чтения и записи по адресам менее 1 МБ направляются в DRAM и из неё. Затем прошивка устанавливает стек и передаёт управление DRAM.
Далее выполняются некоторые инициализации, специфичные для платформы, например, конфигурируется GPIO и повторно активируется сторожевой таймер в ICH, который был отключён до инициализации памяти, прокладывая путь к включению прерываний. Участки локального продвинутого контроллера прерываний (LAPIC) имеются в каждом процессоре, то есть, в многопроцессорной системе они локальны для каждого ЦП. LAPIC определяет, как каждое конкретное прерывание доводится до конкретного ЦП. I/O APIC (IOxAPIC) находится внутри ICH, и существует один IOxAPIC для всех процессоров. Также может быть программируемый контроллер прерываний (PIC), предназначенный для использования в реальном режиме. Ещё есть таблица векторов прерываний, содержащая 256 таких векторов. Это указатели на обработчики соответствующих прерываний. С другой стороны, таблица дескрипторов прерываний применяется для хранения векторов прерываний, когда мы работаем в защищённом режиме.
Затем прошивка устанавливает различные таймеры, зависящие от платформы и от прошивки. Программируемый таймер прерываний (PIT) – это системный таймер, расположенный в IRQ0. Он находится внутри ICH. Таймер событий высокой точности (HPET) также находится внутри ICH, но загрузочная прошивка может его не инициализировать, а позволить ОС установить его, если потребуется. Ещё есть часы (часы реального времени, RTC), которые также находятся в ICH. Есть и другие таймеры, в частности, LAPIC, имеющийся в каждом ЦП. Далее прошивка настраивает кэширование в памяти. В принципе, это сводится к заданию различных характеристик кэша – «обратная запись», «не кэшировать» — для разных диапазонов памяти.
❯ Другие процессоры, устройства ввода-вывода и PCI
Наконец, давайте подключим к работе другие процессоры, поскольку вся описанная выше работа выполнялась на загрузочном процессоре. Для нахождения прикладных процессоров (AP) в том же пакете BSP выполняет инструкцию CPUID. Затем, воспользовавшись своим LAPIC, BSP отправляет каждому AP прерывание SIPI. Каждое SIPI указывает на физический адрес, с которого AP-адресат должен начать выполнение. Стоит отметить, что каждый AP начинает работать в реальном режиме. Следовательно, адрес SIPI обязан быть менее 1 МБ, это максимум адресации в реальном режиме. Обычно, вскоре после инициализации, каждый AP выполняет инструкцию HLT и переходит в состояние останова, ожидая дальнейших инструкций от BSP. Правда, непосредственно перед тем, как ОС берёт на себя управление, AP, предположительно, находятся в состоянии «жду SIPI». В BSP для этого нужно отправить пару внутрипроцессорных прерываний каждому AP.
Далее переходим к устройствам ввода/вывода, например, встроенному контроллеру (EC) и Super I/O, а после этого к инициализации. В принципе, инициализация PCI сводится к:
- Перечислению всех PCI-устройств
- Выделению ресурсов для каждого PCI-устройства
Обсуждаемые здесь вопросы касаются и PCIe. PCI – это иерархическая система шин, где листом каждой шины является или PCI-устройство, или PCI-мост, ведущий к другой PCI-шине. ЦП обменивается информацией с PCI, читая и записывая регистры PCI. Вот какие ресурсы требуются PCI-устройствам для работы: диапазон в адресном пространстве памяти, диапазон в адресном пространстве ввода/вывода и назначение IRQ. ЦП выясняет диапазоны адресов и их типы (отображённые на память или I/O), записывая или считывая регистры базового адреса PCI-устройств. IRQ обычно задаются в зависимости от того, как именно скомпонована плата.
В ходе перечисления PCI прошивка также читает регистр Option ROM. Если этот регистр не пустой, то в нём содержится адрес Option ROM. Это ROM-чип, физически установленный на PCI-устройстве. Например, на сетевой карте может содержаться Option ROM, в котором лежит прошивка iPXE. Встретив Option ROM, считываем его в DRAM и выполняем.
❯ Передача управления загрузчику ОС
Прежде, чем передать управление загрузчику, отвечающему за следующую стадию работы (обычно это загрузчик операционной системы, например, GRUB2 или LILO), прошивка задаёт в памяти некоторую информацию, которой затем воспользуется ОС. В состав этой информации входят, в частности, таблицы ACPI (усовершенствованного интерфейса управления конфигурацией и питанием) и карта памяти как таковой. По карте памяти ОС узнаёт, какие диапазоны адресов предназначены для каких целей. Среди таких регионов – общая память для использования в ОС, относящиеся к ACPI диапазоны адресов, зарезервированные адреса (то есть, не используемые ОС), IOAPIC (будут использоваться IOAPIC), LAPIC (будут использоваться LAPIC). Также загрузочная прошивка устанавливает прерывания для SMM (режима системного управления). SMM – это режим эксплуатации процессоров Intel, наряду с Реальным, Защищённым и Длинным (64-разрядным). ЦП входит в режим SMM, получив SMM-прерывание, которое может быть инициировано по целому ряду причин (например, процессор нагрелся до определённой температуры). Прошивка, прежде, чем передать управление загрузчику ОС, также блокирует некоторые регистры и возможности ЦП, чтобы эти показатели нельзя было изменить уже после вступления ОС в работу.
Сама передача управления загрузчику операционной системы обычно происходит как джамп в соответствующую область памяти. Такой загрузчик ОС как GRUB2 будет действовать на основе записанной в нём конфигурации и, в конце концов, передаст управление операционной системе, например, Linux. В Linux в таком случае обычно используется образ bzImage (большой zImage, а не сжатый bz). Также отметим, что здесь ОС (например, Linux) снова перечислит PCI-устройства и, возможно, продублирует ещё некоторые из тех инициализаций, которые на завершающем этапе выполняла загрузочная прошивка. Обычно Linux выбирает систему, работающую в 32-разрядном режиме при отключенной подкачкой страниц, и выполняет собственные инициализации – например, задаёт страничные таблицы, включает подкачку страниц и переключается в длинный (64-разрядный) режим.