Разработка кода, подготавливающего систему к передаче управления загрузочному образу
В этой статье:
В этом разделе мы подробно рассмотрим устройство начального загрузчика и его модификацию для конкретного оборудования.
Первая задача начального загрузчика — минимальная настройка оборудования и создание среды, в которой будет выполняться сначала программа запуска (например, startup-bios, startup-elbrus и др.), а затем микроядро ЗОСРВ «Нейтрино». Эта задача включает в себя как минимум следующие действия:
Часть начального загрузчика, которая выполняет инициализацию, полностью реализована на языке ассемблера, поскольку выполняется в ПЗУ без участия контроллера памяти. После инициализации оборудования начальный загрузчик вызывает функцию main() для инициализации среды языка C.
После ее подготовки начальный загрузчик может выполнять различные задачи в зависимости от типа устройства, с которого загружается ОС (линейного или с переключением банков).
Под термином «устройство» обычно понимается ПЗУ, в котором хранится образ (флеш-память, EPROM, статическое ОЗУ с питанием от батареи и др.)
Образ может располагаться на следующих линейных устройствах:
Образ может располагаться на следующих устройствах с переключением банков:
Помимо вышеперечисленных устройств, во встраиваемой системе могут использоваться следующие процессоры и конфигурации:
Предположим, что встраиваемая система загружается с устройства с переключением банков или страничной структурой (например, со страничного флеш-накопителя, дискового/сетевого устройства и др.) и образ не сжат. Начальный загрузчик должен выполнить три основных задачи:
После настройки контроллера функция копирует образ в оперативную память через последовательный интерфейс.
unsigned long image_scan( unsigned long start,unsigned long end );
Функция image_scan():
STARTUP_HDR_SIGNATURE
)
int image_setup( unsigned long address );
Функция image_setup():
На этом этапе программа запуска находится в ОЗУ (и должна всегда выполняться в нем), а в ее заголовке указан адрес образа ОС.
![]() | Поскольку программа запуска отвечает за копирование файловой системы образа в конечную область ОЗУ, начальный загрузчик должен помещать образ в место, где программа запуска, которая не знает о страничных устройствах (последовательных, дисковых, параллельных, сетевых и др.), имеет линейный доступ к нему.
Следует иметь в виду, что если образ сжат, начальный загрузчик может копировать сжатый образ в область, которая не пересекается с конечным местоположением образа в ОЗУ после распаковки программой запуска. Если образ находится во флеш-памяти, ПЗУ или на другом линейном накопителе, это не представляет проблему, но если образ хранится на страничном устройстве, разработчику необходимо позаботиться о том, чтобы образ в оперативной памяти не пересекался с образом, распакованным программой запуска. Действуют следующие правила:
|
int image_start( unsigned long address );
Функция image_start() не возвращает значение при успешном выполнении и возвращает -1
в случае ошибки. Она передает управление по адресу startup_vaddr заголовка программы запуска.
В системе, которая загружается с линейного устройства (например, линейного флеш-накопителя, ПЗУ и т.д.) начальный загрузчик выполняет те же задачи, что и при загрузке со страничного устройства, с одним важным исключением: ему не требуется целиком копировать образ ОС с устройства в оперативную память.
Сложность кода начального загрузчика зависит от конфигурации встраиваемой системы. Чтобы описать различные типы начальных загрузчиков, мы воспользуемся терминами «горячий запуск» и «холодный запуск»:
Этот начальный загрузчик не начинает выполняться сразу после сброса, а принимает управление от BIOS или ПЗУ-монитора.
BIOS систем с архитектурой x86 и многие ПЗУ-мониторы поддерживают расширения. При сканировании памяти после включения питания BIOS или ПЗУ-монитор пытается обнаружить расширения в адресном пространстве. Признаком расширения является сигнатура; например, сигнатура расширения PC BIOS состоит из двух байт 0x55
и 0xAA
, которые находятся в начале ПЗУ. Расширение должно быть готово принимать управление по адресу, который определяется смещением точки входа (например, точка входа в расширение PC BIOS имеет смещение 0x0003
).
Следует иметь в виду, что этот метод применяется в различных ПЗУ-расширениях PC BOOTP. После того, как код ПЗУ с сигнатурой расширения получает управление, он загружает образ по сети и помещает его в оперативную память.
Одно из преимуществ ЗОСРВ «Нейтрино», особенно с точки зрения себестоимости встраиваемых систем, заключается в том, что в них не является обязательным наличие BIOS или ROM-монитора. Эта статья предназначена в первую очередь для разработчиков, которые создают собственного начального загрузчика или по какой-либо причине не желают пользоваться штатным начальным загрузчиком BIOS/монитора.
Рассмотрим задачи начального загрузчика для холодного запуска.
При включении питания или сбросе процессора некоторые его регистры находятся в известном состоянии, а управление передается по определенному адресу памяти (вектору сброса).
IPL должен располагаться на векторе сброса и выполнять следующие задачи:
Например, в системе x86 вектор сброса расположен по адресу 0xFFFFFFF0
. Устройство с начальным загрузчиком должно находиться в этом диапазоне адресов. Как правило, в системах x86 PC BIOS вектор сброса содержит инструкцию JMP c последующим переходом на код диагностики, настройки и начальной загрузки.
Независимо от используемого процессора начальный загрузчик должен загружать образ в соответствии с требованиями микроядра ЗОСРВ «Нейтрино», как описано выше. Иногда в коде начального загрузчика необходимо предусматривать возможность загрузки резервного образа (например, .altboot при загрузке с дискеты/жесткого диска) или автоматического отката при повреждении образа.
Следует иметь в виду, что трудоемкость начальной загрузки в значительной степени зависит от местоположения образа.
Еще раз обратимся к двум типам устройств хранения образов.
Это наиболее простой вариант. Образ целиком хранится в ПЗУ или на устройстве PC-Card, адресное пространство которого полностью отображено в адресное пространство процессора. Единственное, что должен сделать начальный загрузчик — скопировать код запуска в оперативную память. Этот способ идеально подходит для компактных или глубоко встраиваемых систем.
Следует обратить внимание, что в архитектуре x86 отсутствует требование адресуемости устройства в первом мегабайте памяти; программа запуска также может располагаться за его пределами.
Обратите внимание, что для полного отображения адресного пространства устройства PC-Card в адресное пространство процессора может потребоваться настройка, которую должна быть выполнена начальным загрузчиком (мы предоставляем библиотечные функции для нескольких стандартных интерфейсных микросхем PC-Card).
Образ хранится на устройстве, которое не отображается непосредственно в линейную память. В этом случае разработчику необходимо решить, как код начального загрузчика будет получать доступ к образу, который хранится на устройстве.
Возможны различные варианты:
Рассмотрим общие характеристики этих способов. В таких системах код начального загрузчика знает, как считывать данные из аппаратных компонентов. Начальный загрузчик выполняет следующие действия:
Образ хранится на твердотельном накопителе (ПЗУ, EPROM, флеш-память), но процессор видит только небольшую часть содержимого устройства. Это устройство имеет небольшое окно, которое отображается в адресное пространство процессора (например, 32 Кбайт), а дополнительные аппаратные регистры определяют, какая часть устройства отображена в этом окне.
Чтобы загрузить образ, начальный загрузчик должен знать, как управлять блоком отображения окон. Затем начальный загрузчик копирует образ из окна в оперативную память и передает ему управление.
![]() | По возможности следует избегать применения блоков отображения памяти (как собственной разработки, так и «стандартных»), поскольку они усложняют программно-аппаратную структуру встраиваемой системы. Мы настоятельно рекомендуем пользоваться линейными устройствами (дополнительную информацию см. в статье Рекомендации по проектированию). |
В зависимости от особенностей встраиваемой системы или процесса ее разработки можно загружать образ по сети через интерфейс Ethernet. ПЗУ-монитор некоторых встраиваемых плат содержит в себе код BOOTP. На ПК с сетевой платой ISA или PCI загрузочное ПЗУ помещается в адресное пространство процессора, где BIOS компьютера передает ему управление. Код BOOTP взаимодействует с сетевым оборудованием и принимает образ от удаленной системы.
Чтобы загрузить ЗОСРВ «Нейтрино» с помощью BOOTP, необходимы ПЗУ с BOOTP для клиентской части (ОС) и сервер BOOTP (например, bootpd). Поскольку протокол TFTP используется для передачи образа от сервера клиенту, также потребуется сервер TFTP, который поставляется вместе с сервером BOOTP в большинстве операционных систем («Нейтрино», UNIX, Windows 95/98/NT/...).
Последовательный порт может быть полезен в процессе разработки целевой системы для скачивания ее образа или обеспечения отказоустойчивости (например, можно повторно скачивать образ через последовательный порт при несовпадении контрольной суммы).
Можно встраивать код загрузки образа через последовательный интерфейс в начального загрузчика, чтобы он мог скачивать образ с внешнего устройства. Как правило, это не приводит к заметному увеличению себестоимости встраиваемой системы; в большинстве случаев можно исключать последовательный порт из окончательной конфигурации. Большинство отладочных плат, изготавливаемых производителями микросхем, оснащены последовательными портами. Мы предоставляем исходный код встраиваемого загрузчика через последовательный порт с микросхемой 8250.
Процесс начальной загрузки системы почти идентичен описанному выше процессу загрузки по сети, однако для скачивания образа используется последовательный порт.
Для загрузки традиционной встраиваемой системы, которая схожа с ПК и оснащена BIOS, легче всего использовать диск. BIOS берет на себя всю необходимую работу — считывает образ с диска, помещает его в ОЗУ и передает ему управление.
С другой стороны, при отсутствии BIOS самостоятельно реализовать этот метод загрузки наиболее сложно, поскольку требуется драйвер для доступа к диску (стандартному жесткому диску с вращающимся накопителем или твердотельному диску). Начальный загрузчик должен считать таблицу разделов устройства, определить, где находится содержимое образа, а затем отобразить его фрагменты в окно и передать байты в ОЗУ (если используется твердотельный диск) либо считать байты данных с дискового носителя.
Если ни один из вышеперечисленных методов загрузки не подходит для разрабатываемой встраиваемой системы, необходимо написать начального загрузчика, который выполняет рассмотренные выше основные действия — обрабатывает вектор сброса, считывает образ с какого-либо накопителя и передает управление программе запуска.
Когда образ загружен в оперативную память или готов к выполнению в ПЗУ, необходимо передавать управление коду запуска (который скопирован из образа в ОЗУ).
Подробную информацию о различных типах программ запуска см. в статье Руководство по разработке модуля startup.
После передачи управления коду запуска начальный загрузчик завершается.
В этом разделе подробно рассматриваются этапы создания начального загрузчика встраиваемой системы, который загружается из ПЗУ или флеш-памяти.
Системы, которые загружаются с диска или по сети, обычно оснащены BIOS или ПЗУ-монитором, в которых уже реализована значительная часть функций начального загрузчика. Если разрабатываемая встраиваемая система относится к этой категории, можно пропустить эту главу и перейти к статье Руководство по разработке модуля startup.
IPL получает управление в момент сброса и выполняет следующие основные функции:
На этом этапе выполняется базовая инициализация оборудования, в том числе получение доступа к оперативной памяти системы, которая может быть недоступной после сброса. Трудоемкость инициализации зависит от действий, которые были выполнены до передачи управления загрузчику. В одних системах этот код активизируется после сброса и решает весь объем задач, а в других — вызывается более компактным загрузчиком, который выполняет часть необходимой работы.
Следует иметь в виду, что на этом этапе не обязательно инициализировать стандартные периферийные устройства (например, скорость работы последовательных портов). Эту задачу выполняют драйверы ОС, которые запускаются позже. Достаточно инициализировать только оборудование, которое позволяет передавать управление программе запуска в образе.
Программа запуска написана на языке C и доступна вместе со всеми исходными кодами. Структура кода запуска позволяет с легкостью модифицировать его и добавлять в него новые инициализации (например, настраивать структуру системной страницы в оперативной памяти).
Код начального загрузчика должен обнаруживать загрузочный образ, который сгенерирован утилитой mkifs, и частично или полностью копировать его в память.
Загрузчик использует информацию в заголовке образа для копирования заголовка и модуля startup-* в оперативную память. Если образ не находится в линейно адресуемой памяти целиком, загрузчик должен помещать его в ОЗУ.
Структура загрузочного заголовка struct startup_header определена в файле <sys/startup.h>
. Она имеет размер 256 байт и содержит следующие поля, которые считываются начальным загрузчиком и/или программой запуска:
Корректность загрузочного образа проверяется путем вычисления контрольной суммы всего образа с помощью функции checksum():
checksum( image_paddr, startup_size );checksum( image_paddr + startup_size, stored_size - startup_size );
В этом разделе описаны некоторые поля, которые используются начальным загрузчиком и программой запуска при различных типах загрузки ЗОСРВ «Нейтрино». Эти поля заполняются утилитой mkifs.
Ранее мы рассмотрели, какие действия выполняются начальным загрузчиком и программой запуска.
На этом рисунке показан образ, выполняемый по месту хранения (XIP, eXecute In Place):
![]() | В следующих примерах псевдокода поле image_paddr соответствует исходному местоположению образа в линейном ПЗУ, а поле ram_paddr — целевому местоположению образа в ОЗУ. |
Ниже перечислены действия начального загрузчика:
checksum( image_paddr, startup_size );checksum( image_paddr + startup_size, stored_size - startup_size );copy( image_paddr, ram_paddr, startup_size );jump( startup_vaddr );
Этот сценарий аналогичен предыдущему, но со сжатым образом:
Ниже перечислены действия начального загрузчика:
checksum( image_paddr, startup_size );checksum( image_paddr + startup_size, stored_size - startup_size );copy( image_paddr, ram_paddr, startup_size );jump( startup_vaddr );
В программе запуска необходимо выполнить следующее действие:
uncompress( ram_paddr + startup_size, image_paddr + startup_size,stored_size - startup_size );
В этом сценарии образ не выполняется по месту хранения:
Ниже перечислены действия IPL:
checksum( image_paddr, startup_size );checksum( image_paddr + startup_size, stored_size - startup_size );copy( image_paddr, ram_paddr, startup_size );jump( startup_vaddr );
В программе запуска необходимо выполнить следующее действие:
copy( ram_paddr + startup_size, image_paddr + startup_size,stored_size - startup_size );
В этом сценарии мы не создаем полнофункционального начального загрузчика. Начальный загрузчик BIOS помещает образ в память и передает управление нашему начальному загрузчику. Поскольку существующий начальный загрузчик не знает, где располагается программа запуска, он всегда передает управление в начало образа. Мы помещаем в начало образа небольшого начального загрузчика, который передает управление по адресу startup_vaddr:
Начальный загрузчик выполняет следующее действие:
jump( startup_vaddr );
Этот сценарий аналогичен предыдущему, но нам необходимо распаковать образ в программе запуска:
Программа запуска выполняет следующее действие:
uncompress( ram_paddr + startup_size, image_paddr + startup_size,stored_size - startup_size );
Загрузка с устройства с переключением банков очень похожа на загрузку с диска / по сети, но в начальном загрузчике необходимо написать код, который копирует образ в ОЗУ:
bankcopy( image_paddr, ram_paddr, startup_size );checksum( image_paddr, startup_size );checksum( image_paddr + startup_size, stored_size - startup_size );jump( startup_vaddr );
Дальнейшие действия сводятся к описанному ранее сценарию загрузки с диска / по сети сжатого или несжатого образа.
При работе с устройством с переключением банков необходимо отображать физические адреса и размеры соответствующим образом. Желаем успехов и больше не занимайтесь переключением банков ПЗУ! Работайте в линейном адресном пространстве.
В этом разделе мы рассмотрим структуру дерева каталогов исходного кода начального загрузчика, а также типового файла с исходным кодом.
Дерево каталогов с исходным кодом IPL имеет следующую структуру:
рабочий_каталог_bsp/src/hardware/ |--> ipl/ | `--> boards/ | |--> xzynq/ | |--> rockchip/ | |--> imx6x/ | |--> imx8x/ | |--> orangepi/ | |--> p3041/ | |--> p5040/ | `--> ... | |--> startup/ `--> flash/
В каталоге рабочий_каталог_bsp/src/hardware/ipl/boards
хранятся исходные коды начальных загрузчиков для конкретных плат (например, каталог рабочий_каталог_bsp/src/hardware/ipl/boards/xzynq
содержит исходный код для материнской платы Xilinx Zynq UltraScale+ MPSoC на процессоре ARMv8 Cortex-A53 с архитектурой AArch6)).
Код начального загрузчика состоит из двух уровней. Первый уровень написан на языке ассемблера и предназначен для подготовки среды выполнения кода второго уровня, написанного на языке C. Обычно на первом уровне настраиваются контроллеры DRAM, инициализируются различные регистры и линии выбора микросхем для обращения к периферийным устройствам.
Обычно ассемблерный код начального загрузчика находится в файле, имя которого начинается с init
(например, init8xx.s
для платы MPC8xxFADS); файл C всегда называется main.c
.
После того, как ассемблерная программа завершает минимальный набор подготовительный действий для передачи управления C-программе, программа main() вызывает следующие функции в указанном порядке:
Рассмотрим программу main.c
для платы FADS8xx:
#include "ipl.h"unsigned int image;int main( void ){/* Поскольку образ находится по адресу 0x2840000, нам не нужно вызывать функцию image_download_8250 */image = image_scan( 0x2840000, 0x2841000 );/* Копирование в ОЗУ программы запуска, которая выполняет все необходимые действия с образом */image_setup( image );/* Настройка регистра связи и переход к начальному адресу программы запуска */image_start( image );return (0);}
Поскольку в этом примере мы работаем с линейно адресуемым устройством флеш-памяти, на котором находится образ, нам не нужно вызывать функцию image_download_8250().
Далее вызывается функция image_scan(), которой мы передаем очень узкий диапазон адресов для поиска образа, поскольку знаем, где он находится.
Затем вызывается функция image_setup() с адресом, полученным от функции image_scan(). Функция image_setup() копирует код запуска в ОЗУ.
В конце вызывается функция image_start(), которая передает управление программе запуска. Мы не ждем возврата из нее и используем оператор
return (0);
только для того, чтобы избежать ошибки компилятора из-за отсутствия возвращаемого значения в функции main().
Рекомендуется начинать разработку нового IPL с изучения уже готового загрузчика для процессора и платы, которые максимально схожи с целевыми.
Выполните следующие действия:
каталоге рабочий_каталог_bsp/src/hardware/ipl/boards
и присвойте ему имя платы
Предыдущий раздел: перейти