Как написать загрузочный файл

Обновлено: 04.07.2024

Компьютеры и BIOS-ы создают одни компании, а Операционные системы пишут другие компании. Поэтому, чтобы процессор перешел от выполнения инструкций BIOS к операционной системе, предусмотрена не простая передача управления, а целая процедура (программа). Эту процедуру пишут разработчики Операционной системы, а не производители компьютера. Эта процедура называется Boot Loader (Загрузчик), хранящийся на носителе, с которого загружается Операционная система. Наиболее известными из до сих пор поддерживаемых носителей, с которых может загружаться Операционная система, являются Floppy Disk-и (дискеты). Сегодня, правда, Floppy Disk-и вытеснены новыми устройствами, типа CD/DVD приводов или USB Drive-ами. При экспериментах удобно использовать не настоящий компьютер, а виртуальную машину. Виртуальная машина вместо настоящих дискет использует обычные файлы, в которых хранится содержимое загрузочной дискеты, поэтому несмотря на то, что дискеты устарели, с ними (виртуальными), по прежнему удобно экспериментировать. На картинке настройки моей виртуальной машины. Видно что в качестве floppy-диска подключен файл “D:\VM\hw.flp”.


Первый эксперимент

Программа непосредственно из статьи:

Я поместил этот исходный код в файл hw.asm (hello world), и скомпилировал:

hello world output

Загрузка процессора моего компьютера, на котором работает VMWare оказалась 50%:


Стоит отметить, что на ноутбуке, на котором я тестируюсь, установлен двухядерный процессор Intel. 50% загрузка означает 100%-ю загрузку одного из ядер. Случайно ли это?

Код примера заканчивается бесконечным циклом
jmp $ ; вечный цикл

Этот цикл и грузит одно из ядер процессора на 100%. Как этого избежать? Вспомним утверждение “Процессор просто выполняет последовательность инструкций, пока его не выключат или не встретится специальная инструкция HLT”.

Проверю это утверждение. Заменю вечный цикл на 2 инструкции:

cli ; запрещаю аппаратные прерывания
hlt ; останавливаю процессор до возникновения аппаратного прерывания

hlt приводит к остановке выполнения команд процессором до ближайшего аппаратного прерывания. Но, так как аппаратные прерывания запрещены предыдущей командой, то процессор просто прекращает выполнение программы. Останавливается. Тест показывает, что полученная на экране картинка всё та же: “Hello World!”. Зато загрузка процессора 0%.

Скомпилированный hw.flp выглядит примерно так (я использую для просмотра far).

boot sector hex dump

В 512-байтном пространстве Boot Loader-a остается ещё много места для размещения настоящего Boot Loader-a, который загрузит “настоящую” Операционную систему.

Некоторые шаги экспериментов (например контроль загрузки процессора) легко сделать только в условиях виртуальной машины. На настоящей машине, если загрузчик подменен на вышеописанную программу “Hello World!”, загрузку процессора можно оценить только по громкости вентилятора процессора. (Поэтому сложно проверить остановку процессора инструкциями cli; hlt).

Что дальше?

Для начала подведем итоги. Что известно про Boot Loader:
Известно, что программа эта загружается по адресу 0x7c00
Известно, что размер загрузчика 0x200 (512) байт
Известно, что процессор Intel работает в реальном режиме
Известно, что загрузчику доступны для использования функции BIOS
К функциям BIOS можно обратиться с помощью присвоения значений определенным регистрам и последующим вызовом прерываний.

Например, в приведенном примере вывод на экран осуществляется с помощью прерывания int 0x10. Последовательность команд выведет символ ‘H’ в текущую позицию курсора.

Изучение исходников разных Boot Loader-ов показало, что неизвестно куда указывает стек в момент перехода на адрес 0x7c0:0. Поэтому загрузчик в своем коде устанавливает регистр стека как удобно. (Например сразу перед загрузчиком).

В последствии, я провел эксперимент по определению состояния регистров процессора, в момент старта Boot Loader-a. См. статью по этой ссылке.. Оказалось, что в VMWare, например, при загрузке с дискеты стек указывает 0:3EC, что соответствует верхней части таблицы прерываний. ax=0x7C03 ds=0x40 sp=0:3EC, остальные регистры равны 0. В регистре dl BIOS передает идентификатор диска с которого загружался загрузочный сектор. Для диска A: (дискеты) это значение 0.

Приветствую всех своих читателей!

В этом выпуске мы приступим к написанию не очень сложной, но достаточно важной части любой операционной системы - начального загрузчика. Именно эта часть обычно присутствует абсолютно во всех рассылках, но её не стоит пропускать, потому что без загрузчика мы не сможем производить какие-либо действия. Можно очень много рассуждать о структуре ядра ОС, однако без загрузчика мы никак не сможем проверить свои идеи на практике. Конечно, существуют универсальные загрузчики вроде GRUB, но хотелось бы рассмотреть аспект написания системы наиболее полно. Тем, кому интересует исключительно программирование самого ядра, придётся подождать пока мы научимся его запускать :-)

Теория

Итак, начнём с теоретической части. Весь обмен данными с носителями информации вроде дискет, жёстких дисков и флешек осуществляется только блоками фиксированными размера - секторами. Как в памяти компьютера нельзя непосредственно обратиться к единице меньшей, чем байт, так на диске все операции чтения и записи выполняются посекторно. Самый часто используемый размер сектора - 512 байт, хотя есть и немногочисленные исключения - на CD-дисках, например размер сектора 2 килобайта. Но сейчас мы не рассматриваем загрузку с последних, так что можно считать, что размер сектора ровно 512 байт, не больше и не меньше.

После включения компьютера управление получает BIOS - Basic Input-Output System. Он проводит первичное тестирование оборудования, предоставляет интерфейс для настройки некоторых компонентов (например, часов) и наконец загружает 0-ой сектор загрузочного диска (определяется в настройках), передавая ему управление. Поскольку 512 байт слишком мало, чтобы там можно было разместить полноценный драйвер для работы с диском, BIOS предоставляет все необходимые функции для работы с экраном, клавиатурой и мышью. Их более чем достаточно для любого начального загрузчика.

В последнее время внедряется альтернативная система инициализации - EFI (Extensible Firmware Interface). Возможно, когда-нибудь мы рассмотрим и её, но не раньше, чем напишем ядро ОС. С одной стороны эта система отбрасывает некоторые устаревшие технологии, но с другой стороны требует больших теоретических знаний для начала работы. К тому же подавляющее большинство версий EFI на сегодняшний день поддерживают режим эмуляции BIOS и наша система сможет работать и с ними. Да и пока достаточно мало виртуальных машин поддерживают загрузку EFI-совместимых систем.

Практика

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

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

Как я уже сказал выше, BIOS загружает в оперативную память первые 512 байт диска и передаёт им управление. Наш код оказывается в реальном режиме работы процессора (те, кто не знают что это, идут читать какой-нибудь учебник по Assembler) по адресу 0000:7C00. Прерывания запрещены, в регистре DL находится номер загрузочного диска (например, 0 и 1 для дискет, начиная с 0x80 идут все прочие виды дисков). Некоторые сегментные регистры указывают на область данных BIOS, другие регистры также могут содержать дополнительную информацию, но на это лучше не рассчитывать, потому что многое зависит от деталей реализации конкретного BIOS. Также стоит отметить, что последние два байта начального загрузчика должны содержать сигнатуру 0x55,0xAA, иначе многие BIOS посчитают такой загрузчик некорректным и откажутся запускать.

Начнём с заготовки загрузчика, который способен просто запуститься, настроить всё окружение, вывести пару строчек, подождать нажатия на клавиатуру и перезагрузить машину при любом нажатии. На этом пока и остановимся, иначе этот выпуск рассылки получится слишком длинным. Загрузчик будет выполнять некоторые ненужные сейчас действия (например, запоминание номера диска, с которого мы стартовали), но они нам пригодятся, когда в следующей части мы напишем его полноценную (способную что-то загрузить) версию.

Опишу некоторые аспекты работы этого кода.

Обычно, после первых 3-4 байт загрузчика размещает заголовок некоторых файловых систем (например, FAT), поэтому первым делом мы обходим все данные и прыгаем на истинную точку входа начального загрузчика. Специальное слово word говорит flat assembler не пытаться оптимизировать размер перехода и в любом случае положить адрес перехода в 2 байта, даже если можно в 1. Таким образом наш jmp должен гарантированно занять 3 байта.

Также в результате этого действия у нас первые 3 байта загрузчика содержат ненужные для дальнейшей работы данные, поэтому мы можем сэкономить 3 байта (512 это очень мало и любую возможность оптимизировать размер без потери функционала не следует упускать), разместив там какие-нибудь неинициализированные переменные. Пока она одна: номер загрузочного диска - байтовая переменная disk_id.

BIOS предоставляет достаточно много функций, все они вызываются с помощью программных прерываний, для осуществления которых служит ассемблерная инструкция int. Она принимает в качестве аргумента номер прерывания. За каждым прерыванием закреплён адрес функции обработки. Она принимает параметры в регистрах процессора, выполняет действие и возвращает управление нашему коду. В данном случае нас интересует сервис с кодом 0x10. Это прерывание служит для управления экраном. Номер нужной функции прерывания передаётся в регистре AH. Функция write_str использует функцию с кодом 0x0E, которая просто выводит символ из AL на экран со сдвигом курсора (воспринимаются также различные управляющие коды вроде последовательности 13,10 - перевод строки и возврат каретки). Наш код сохраняет оба модифицируемых во время работы регистра - AX и SI, поэтому его можно вызывать из любых мест кода не беспокоясь о последствиях.

Вторая функция - error. Она не используется в текущем коде, не пригодится в будущем. Это один из примеров оптимизации размера. Как известно, команда call помещает в стек адрес возврата, то есть адрес, следующий за этой инструкцией. Но если возврат из функции не предполагается, то там могут находиться произвольные данные, к которым можно получить доступ вытолкнув адрес из стека. В этом случае мы экономим на команде запихивания в стек этого адреса, потому что этим занимается процессор. Функция error нужна для обработки критических ошибок загрузки (а других на этом этапе и не бывает :-) ), например ошибки чтения диска. Использовать эту функцию следует так:

Основной код располагается за меткой boot. Первым делом он обнуляет все сегментые регистры, чтобы правильно работала адресация данных. Затем он настраивает стек и разрешает прерывания. Стеком будет считаться область от данных BIOS (0000:0400) до нашего кода. Это более 30 килобайт, что вполне достаточно.

Далее с помощью write_str выводится название загрузчика и наконец работа завершается переходом на reboot (нам больше нечего делать - мы пока ничего не умеем загружать).

Компиляция и запуск

Для компиляции достаточно просто выполнить команду:

В итоге должен получиться файл boot.bin размером 512 байт. Некоторые эмуляторы (например, Bochs) позволяют подключать неполные образы дискет. Просто отсутствующие сектора не будут загружаться.

Затем можно запускать всё это в Bochs и, если не было ошибок, вы увидите две строки.


Конфиг для Bochs:

Могу вас поздравить: вы написали свой первый начальный загрузчик!

Заключение

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

Это статья была написана для людей, которым всегда интересно знать как работают разные вещи. Для тех разработчиков которые обычно пишут свои программы на высоком уровне, C, C++ или Java — не важно, но при этом столкнулись с необходимостью сделать что-то на низком уровне. Мы будем рассматривать низкоуровневое программирование на примере работы bootloader-а.

Мы опишем что происходит после включения компьютера и как система загружается. В качестве практического примера, рассмотрим как вы можете написать свой собственный загрузчик, который фактически является отправной точкой при загрузки системы.

Что такое Boot Loader

Boot loader — это программа, которая записана на первом секторе жесткого диска. BIOS автоматически считывает всё содержимое первого сектора в память, сразу после включения питания. Первый сектор также называется главной загрузочной записью. На самом деле, это не является обязательным для первого сектора жесткого диска для загрузки чего либо. Это название было исторически сложившиеся, так как разработчики использовали такой механизм для загрузки операционной системы.

Будьте готовы погружаться глубже

В этом разделе я расскажу о том, какие знания и инструменты, необходимы для разработки собственного загрузчика, а также напомню некоторую полезную информацию о загрузке системы.

И так, какой язык Вы должны знать, чтобы написать Boot Loader

В первую очереди, при работе компьютера, контроль аппаратного обеспечения осуществляется преимущественно посредством функций BIOS, известный как «прерывания». Вызвать прерывание можно только на ассемблере — будет здорово если вы хоть немного знакомы с этим языком. Но это не обязательное условие. Почему? Мы будем использовать технологию «смешанного кода», где можно совместить высокоуровневые конструкции с командами низкого уровня. Это не много упрощает нашу задачу.

В этой статье в основном будет использоваться язык C++. Но если вы знаете C, то вам будет легко узнать необходимые C++ элементы. В общем, даже знание языка C будет достаточно, но тогда вам придется изменять исходный код примеров.

И так, для разработки простого загрузчика вы должны знать, C или C++, а также было бы не плохо если вы знаете немного Ассемблера.

Какой компилятор вам нужен

Чтобы использовать технологию смешанного кода, нужно по крайней мере два компилятора: для ассемблера и для C/C++, а также компоновщик который объединит объектные файлы(.obj) в один исполняемый файл.

Теперь, давайте поговорим о некоторых особых моментов. Есть два режима функционирования процессора: реальный и защищенный режим. Реальный режим является 16-битным и имеет некоторые ограничения. Защищенный режим является 32-битным и полностью используется операционной системой. Когда компьютер только начинает работу, процессор работает в 16-битном режиме. Таким образом, чтобы написать программу и получить исполняемый файл, вам понадобится компилятор и компоновщик для ассемблера для 16-битного режима. Для C/C++ вам потребуется только компилятор, который умеет создавать объектные файлы для 16-битного режима.

Современные компиляторы сделаны для 32-разрядных приложений, поэтому мы не сможем их использовать.

Я пробовал несколько бесплатных и коммерческих компиляторов для 16-битного режима и выбрал продукт от Microsoft. Компилятор вместе с компоновщиком для ассемблера, C и C++ включены в Microsoft Visual Studio 1.52, его можно скадать с официального сайта компании. Некоторые подробности о компиляторов которые нам нужны приведены ниже.

ML 6,15 — компилятор ассемблера от Microsoft для 16-битного режима.
LINK 5,16 — компоновщик, который умеет создавать COM-файлы для 16-битного режима.
CL — С, С++ компилятор для 16-битного режима.

Вы также можете использовать несколько альтернативных вариантов.

DMC — бесплатный компилятор для компиляции ассемблера, C, C++ для 16 и 32-битном режиме Digital Mars.
LINK — бесплатный компоновщик для компилятора DMC.

Есть также некоторые продукты от Borland.

BCC 3,5 — С, С++ компилятор, который умеет создавать файлы для 16-битного режима.
TASM — компилятор асемблера для 16-битного режима.
TLINK — компоновщик, который может создавать файлы COM для 16-битного режима.

Все примеры кода в этой статьи, были разработаны с инструментами от Microsoft.

Как система загружается

Для того, чтобы решить нашу задачу мы должны вспомнить, как система загружает.
Рассмотрим кратко, как компоненты системы взаимодействуют при загрузке системы.


После того, как управление было передано по адресу 0000:7C00, Master Boot Record (MBR) начинает свою работу и запускает загрузку операционной системы.

Давайте перейдем к кодированию

В следующих разделах мы будем непосредственно заняты низкоуровневым программированием — мы будем писать наш собственный загрузчик.

Архитектура программы
  1. Правильная загрузка в память по адресу 0000:7 C00.
  2. Вызов BootMain функции, которую мы написали в языке высокого уровня.
  3. Вывести на дисплей фразу — ”Hello, world…", from low-level.


Первый объект это StartPoint, который написан исключительно в ассемблере, поскольку в языках высокого уровня нет необходимых нам инструкции. Это говорит компилятору, какой тип памяти должен использоваться, и адрес команды в RAM, которая должна выполняться после его чтения. Он также исправляет регистры процессора и передает управление функции BootMain, которая написано на языке высокого уровня.

Следующий объект- BootMain — является аналогом main, что, в свою очередь является основной функцией в которой сконцентрированы все функции программы.

Среда разработки

Здесь я использую стандартные среды разработки Microsoft Visual Studio 2005 или 2008. Вы можете использовать любые другие инструменты, но я уверен, что эти два, с некоторыми настройками, компилируют и работают легко и удобно.

Сначала мы должны создать проект Makefile Project, где будет выполнена основная работа.


File->New\Project->Общие\Makefile Project

BIOS прерывания и очистка экрана

BIOS предлагает ряд прерываний для работы с железом, таким как видеокарта, клавиатура, системный диск. Каждое прерывание имеет следующую структуру:


Где «number_of_interrupt» является числом прерывания.

Каждое прерывание имеет некоторое количество параметров, которые должны быть установлены до его вызова. Регистр процессора — ah, всегда несет ответственность за количество функций для текущего прерывания, и другие регистры обычно используются для других параметров текущей операции. Давайте посмотрим, как работа прерывания номер 10h выполняется на ассемблере. Мы будем использовать 00-функцию, она меняет видео режим и очищает экран:

Мы будем рассматривать только те прерывания и функции, которые будут использоваться в нашем приложении. Нам понадобится:

«Смешанный код»

Компилятор C++ поддерживает встроенный Ассемблер, то есть при написании кода на языке высокого уровня вы можете также использовать язык низкого уровня. Инструкции ассемблера, которые используются на высоком уровне, также называют asm вставками. Они состоят из ключевого слова "__asm" и блока ассемблерных инструкций:


Чтобы продемонстрировать пример смешанного кода мы будем использовать ранее упомянутый код на ассемблере, который выполняет очистку экрана и объединим его с кодом написанный на C++.

CString реализация

CString класс предназначен для работы со строками. Он включает в себя метод Strlen(), который получает в качестве параметра указатель на строку и возвращает количество символов в этой строке.

  1. TextOut() — выводит строку на экране.
  2. ShowCursor() — управляет курсором представления на экране: показать, скрыть.
  3. ClearScreen() — изменяет видео режим и таким образом очищает экран.
CDisplay — реализация
Types.h — реализация

Types.h является заголовочным файлом, который включает определения типов данных и макросов.

BootMain.cpp — реализация

BootMain() является основной функцией программы, которая является первой точкой входа (аналог main()). Основная работа проводится здесь.

StartPoint.asm — реализация

Давайте соберем все

Создание COM файла

Давайте лучше автоматизируем этот процесс. Для этого нам нужно созать .bat файл и записать в нем нужные команды с необходимыми параметрами.

Поместите компиляторы и компоновщик в каталог проекта. В том же каталоге, мы создаем командный файл и заполняем в соответствии с примером (можно использовать любой каталог вместо VC152, главное чтобы компиляторы и компоновщик находились в нем).:

Ассамблирование — автоматизация

В качестве заключительного этапа в этом разделе мы опишем как превратить Microsoft Visual Studio 2005, 2008, в среду разработки с поддержкой любого компилятора. Для этого нужно перейти в свойствах проекта: Project->Properties->Configuration Properties\General->Configuration Type.

Вкладка Configuration Properties включает в себя три пункта: General, Debugging и NMake. Выберите NMake и укажите путь к «build.bat» в Build Command Line и Rebuild Command Lin.

Если все сделано правильно, то вы можете скомпилировать нажав клавиши F7 или Ctrl + F7. При этом вся сопутствующие информация будет отображаться в окне вывода. Основным преимуществом здесь является не только автоматизация сборки, но и мониторинг ошибок в коде если они будут.

Тестирование и демонстрация

Этот раздел расскажет, как увидеть сделанный загрузчик в действии, как выполнить тестирование и отладку.

Как проверить загрузчик

Вы можете проверить загрузчик на реальном оборудовании или с использованием разработанных для этих целей виртуальных машинам — VMware. Тестирование на реальном оборудовании дает вам больше уверенности, что он работает также как и на виртуальной машине. Конечно, мы можем сказать, что VmWare отличный способ для тестирования и отладки. Мы рассмотрим оба метода.

Прежде всего, нужен инструмент, чтобы записать наш загрузчик на виртуальный или физический диск. Насколько я знаю, есть несколько бесплатных и коммерческих консолей и GUI приложений. Я использовал Disk Explorer для NTFS 3.66 (версия для FAT, называется Disk Explorer для FAT) для работы в ОС Windows и Norton Disk Editor 2002 для работы в MS-DOS.

Я опишу только Disk Explorer для NTFS 3,66 потому что это самый простой способ и подходит для наших целей больше всего.

Тестирование с помощью виртуальной машины VmWare
Создание виртуальной машины

Нам понадобится VmWare версия программы 5.0, 6.0 или выше. Чтобы проверить загрузчик мы создадим новую виртуальную машину с минимальным размером диска, например, 1 Gb. Отформатируйте его в файловую систему NTFS. Теперь нам нужно отобразить отформатированный жесткий диск на VmWare в качестве виртуального диска. Для этого выберите:

File->Map or Disconnect Virtual Disks.

После этого появится окно. Там вы должны нажать кнопку «Map». В следующем появившемся окне вы должны указать путь к диску. Теперь Вы также можете выбрать букву диска.

Не забудьте снять флажок «Open file in read-only mode (recommended)». После того как выполнили все выше описанные индикации диск должен быть доступен в режиме только для чтения чтобы избежать повреждения данных.

После этого мы можем работать с диском виртуальной машины, как с обычными логическим диском в ОС Windows. Теперь мы должны использовать Disk Explorer для NTFS 3,66 чтобы записать загрузочную запись с позиции 0.

Работа с Disk Explorer для NTFS

После запуска программы мы идем в наш диск (File-> Drive). В появившемся окне идем в раздел логические диски и выбираем наш созданный диск(в моем случае это Z).

Теперь мы выбираем меню пункт View как Hex команды. В это появившемся окне мы можем видеть информацию диска в 16-разрядном представлении, разделенная на сектора. Сейчас у нас только 0-ли, так как диск пуст, пока что.

Сейчас мы должны записать наш загрузчик в первый сектор. Мы устанавливаем маркер в положение 00, как это показано на предыдущей картинке. Чтобы скопировать загрузчик мы используем пункт меню Edit->Paste from file command. В открывшемся окне укажите путь к файлу и кликните Open. После этого содержимое первого сектора должен измениться и выглядеть, как это показано на картинке — если вы, конечно, ничего не меняли в коде.

Вы также должны записать подпись 55AAh по позиции 1FE от начала сектора. Если вы не сделаете это, BIOS проверит последние два байта, и не найдя указанную подписи, будет считать что этот сектор не является загрузочным и не загрузит его в память.

Для переключения в режим редактирования нажмите клавишу F2 и напишите необходимые номера — 55AAh подписи. Чтобы выйти из режима редактирования нажмите ESC.

Теперь нам нужно подтвердить записанные данные.

Чтобы применить записанное мы идем в Tools-> Options, теперь мы идем в пункт Mode и выбираем метод записывания — Virtual Write и нажмите кнопку Write.

Большую часть рутинных действий закончили, наконец, и теперь вы можете видеть, что мы разработали с самого начала этой статьи. Давайте вернемся к VwWare чтобы отключить виртуальный диск (File->Map or Disconnect Virtual Disks … and click Disconnect).

Давайте запустим виртуальную машину. Мы видим теперь, как из глубины царства машинных кодов появляются знакомые строки — «Hello World… », from low-level. ".

Тестирование на реальном оборудовании

Тестирование на реальном оборудовании почти такая же, как и на виртуальной машине, за исключением того, что если что-то не работает, вам потребуется намного больше времени, чтобы восстановить ее, чем создать новую виртуальную машину. Чтобы проверить загрузчик не имея возможность потерять данные (все может случиться), я предлагаю использовать флэш-накопитель, но сначала вы должны перезагрузить компьютер, зайдите в BIOS и убедитесь, что он поддерживает загрузку с флэш-накопителя. Если он поддерживает его, то все в порядке. Если нет, то вы должны ограничить тестированием на виртуальной тестовой машине.

Процесс написание загрузчика на флэш-диск в Disk Explorer для NTFS 3,66 такой же как и для виртуальной машины. Вы просто должны выбрать сам жесткий диск вместо своего логического раздела.

Заключение

В этой статье мы рассмотрели, что такое загрузчик, как работает BIOS, и как компоненты системы взаимодействуют при загрузке системы. Практическая часть дала нам понимание о том, как можно разработать свой собственный, простой загрузчик. Мы продемонстрировали технологию смешанного кода и процесс автоматизации сборки с использованием Microsoft Visual Studio 2005, 2008.

Конечно, это лишь малая часть по сравнению с огромной темой низкоуровневого программирования, но если вам было интересна это статья — то это круто.

Конечно, у конечного пользователя вряд ли окажется под рукой программатор, да и вверять ему такой критический процесс опасно. По закону Мёрфи в руках неопытного пользователя гарантированно что-то пойдет не так. Плюс ко всему, прошивку придется поместить в открытый доступ 2 , а значит, недобросовестные конкуренты смогут просто скопировать устройство.

Для упрощения процесса перепрошивки используется загрузчик (англ. bootloader, от bootstrap, петля сзади ботинка, + loader, загрузчик). С его помощью устройство способно само себя прошить, зачастую без разбора корпуса и даже без подключения каких-либо проводов, по воздуху. Загрузчик — это не что иное, как программа, которая загружается раньше основного приложения и принимает решение о необходимости обновляться (или чего-то еще, например, произвести диагностику оборудования 3 ). Прошивку он может брать из разных источников: из памяти или по интерфейсу передачи данных (UART, SPI, I^2^C, CAN, USB и т.д.) от другого устройства или узла. В большинстве микроконтроллеров реализация загрузчика уже имеется — например, в Arduino (используется микроконтроллер AVR от компании Atmel) загрузчик позволяет заливать образ прошивки через UART, лишая при этом возможности отлаживать программу, т.е. вы не можете остановить выполнение программы и посмотреть состояние регистров, значение переменных и т.д. Микроконтроллеры STM32 могут загружаться из трех разных мест 4 , в зависимости от логических уровней на ножках boot0 и boot1 . Если boot0 подтянута к земле, то загрузка начинается из флеш-памяти, начиная с адреса 0x08000000 (обычный режим работы). Если boot1 подтянута к земле, а boot0 к питанию, то загрузка начинается из системной области памяти, которую нельзя изменить. Там хранится записанный на заводе UART-загрузчик. Если обе ножки подтянуты к питанию, микроконтроллер попробует запуститься, считывая прошивку из оперативной памяти, начиная с 0x20000000 .

В идеале, загрузчик должен находиться в недоступной для записи области, так как его главная задача — обновить и/или запустить пользовательское приложение. (Сам загрузчик обычно не обновляется, так как это чревато превращением устройства в кирпич.) С учетом способов загрузки выбор невелик: самописный загрузчик придется записывать и запускать как обычную программу, а уже затем передавать управление приложению. По сути, придется создавать два приложения.


На крайнем левом рисунке изображено классическое приложение, т.е. из таких, которые мы реализовывали до этого в главах про машину состояний и ОСРВ. Карта памяти посередине иллюстрирует работу с системным загрузчиком, который находится в области памяти, доступной только для чтения. И последняя картинка, справа, это тот случай, когда в программе используется самописный загрузчик.

Так как загрузчик располагается в памяти до самой прошивки, то его стоит делать настолько маленьким, насколько это возможно. Стоит, однако, обратить внимание: во флеш-памяти нельзя стереть произвольный бит. Особенности реализации позволяют стирать только всю страницу (англ. page) целиком, и в нашем конкретном случае это 1 Кб памяти, а в stm32l4, например, — это 2 Кб. Таким образом, минимальный размер загрузчика 1 Кб: даже если вы уместите его в 100 байт, остальные 924 байта вы использовать не сможете, не затерев при этом загрузчик. В случае если он не поместился в 1024 и занял 1025 байт, под загрузчик придется отвести уже 2 страницы.

Концептуально всё очень просто, но что бы написать загрузчик, придётся разобраться в процессе запуска микроконтроллера чуть детальнее. Перечитайте подглаву «Компоновщик», дабы освежить память. Как мы уже там упомянули, хоть формально точкой входа в программу служит функция main() , но на самом деле до неё выполняется обработчик сброса, Reset_Handler . Он выставляет адрес стека (MSP), _estack берётся из скрипта компоновщика, и передаёт управление функции LoopCopyDataInit .

Читайте также: