image

Когда мы включаем компьютер, он успевает совершить несколько этапов работы ещё до того, как загрузится операционная система. В этом посте будет рассмотрено, как загружается типичный процессор с архитектурой 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 сводится к:

  1. Перечислению всех PCI-устройств
  2. Выделению ресурсов для каждого 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-разрядный) режим.



Возможно, захочется почитать и это:



Комментарии (11)


  1. Akina
    22.08.2023 08:39

    Как только получено питание (от батареи или электросети), включается Intel ME. Он выполняет свой набор процедур инициализации

    Да щазз! Напомню, что между микросхемой BIOS, в которой записан код Intel ME, и процессором имеется как минимум два устройства - шинный контроллер и контроллер доступа к памяти. И прежде чем процессор получит доступ к коду из микросхемы, который он должен начать выполнять, ему просто необходимо запустить, настроить и протестировать эти промежуточные компоненты.


    1. vilgeforce
      22.08.2023 08:39
      +2

      И тут возникает вопрос: откуда процессор берет код для настройки этих промежуточных компонентов?


      1. taujavarob
        22.08.2023 08:39

        Кроме как иметь этот код в самом процессоре, заранее прошитым, больше ему негде быть. Поди. :-0


      1. 6p45c
        22.08.2023 08:39

        Если не ошибаюсь, в процах есть preboot, она же пзу, она то и выполняет сие действие.


    1. netch80
      22.08.2023 08:39

      И прежде чем процессор получит доступ к коду из микросхемы, который он
      должен начать выполнять, ему просто необходимо запустить, настроить и
      протестировать эти промежуточные компоненты.

      ME выполняется не основным процессором, а процессором южного моста.

      Программная начальная настройка контроллеров памяти и шины не нужна, потому что по включению их регистры сбрасываются по сигналу Reset в нужное состояние (на нужный участок адресного пространства подключена микросхема BIOS через соответствующий мост).

      Описание в статье ужасно, но именно этот момент описан без прямого вранья.


    1. Void7000
      22.08.2023 08:39

      Шинный контроллер давно специализированный для каждого компа, и не шьётся через биос как 8028 или 8038 в пк хт ;)) если имелся ввиду шинный буфео, то это усилитель шины, полностью прозрачный для программ и логики процессора (не требуется его шить на работу (само наличие этой микросхемы предполагает такую карму кристалла зашитую уже на заводе (fab), а контроллер доступа к памяти (dma) это вообще параллельное процессору устройство, которое может обходится без процессора и первоначально служило для ускорения пересылок внутри компа (а вовсе не от процессора куда либо), например от диска данных в память озу, или из памяти озу в сеть (контроллер lan).

      Это означает, что вы программист, не понимающий железо ;) и пишущий гдето там далеко, на языках высокого уровня ;)

      Лучше программистам в железо не соваться. Никогда. Вы постоянно пишите какую то дурь и альтернативно одарённую чушь. Иначе вам придётся постоянно банить железячников. Впрочем за 10 лет вы здесь, на хабре, в этом преуспели ;))))


  1. DMGarikk
    22.08.2023 08:39
    +4

    В типичном BIOS находится флеш-дескриптор… и GbE (gigabit ethernet).

    что в типичном BIOS-е находится?


    1. RikoMinase
      22.08.2023 08:39

      GbE-регион действительно может иметься, но в случае, если на материнской плате стоит сетевая карта от Intel. Данный регион используется для того, что бы заводской эксплойт Intel ME мог иметь доступ к сетевому стеку.


      1. DMGarikk
        22.08.2023 08:39

        просто звучит крайне странно, именно GbE? а Fast Ethernet (FE) может быть?
        очень странное название конкретной технологии, к биосу компа имеющую отошение примерно никакое


  1. netch80
    22.08.2023 08:39
    +3

    Насколько я в курсе, платформы с Intel ME (или эквивалентом AMD) и без него совершенно по-разному ведут себя по отношению к оперативной памяти. Если есть ME, то поиск модулей памяти и первичное конфигурирование контроллеров (и не только - проверяется и подключается вся периферия, которая должна работать при выключенных процессорах) исполняет он, BIOS в процессоре пользуется готовыми данными. А вот если ME нет, то включается описанный набор хитрых приёмов с использованием кэша в качестве эмуляции оперативной памяти. Прошу уточнить, кто знает точно.

    Рассказ про адрес загрузки BIOS, 32-битный режим и всё такое - малая и не самая характерная часть общего рассказа без упоминания хотя бы "unreal mode" и стартового режима работы контроллера памяти, так что выглядит слишком изолированным. Аналогично про управление прерываний.

    Микрокод может обновляться и через BIOS, и через ОС, так что про "очень раннюю стадию" - полуправда. В микрокоде очень много не кода, а флагов типа "для операции X использовать модуль X1, а не X2".

    В целом - статья выглядит как взяли толстую книгу и рандомно выдернули несколько глав из неё, потеряв общую целостность. Качество на двойку с плюсом.


  1. rus-spb
    22.08.2023 08:39

    Если у нас стартовый адрес 0x000F.FFF0, (или FFFF.FFF0), у нас есть всего 16 байт на код до возникновения события переполнения указателя адреса.
    Вот аккуратно там FAR JMP и помещается в этих первых байтах. В реальном режиме эта команда 5-ти байтная.
    Это всегда самая первая команда (за редкими исключениями спец-стендов энтузиастов), с которой начинается выполнение кода любого биоса на процессорах х86 после а) подачи питания и б) снятия ресета.
    "Чип биоса" не настраивается никаким боком, это "загружаемая в него прошивка настраивается таким образом, что...". Очень неудачный, вводящий в заблуждение перевод.