Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Пишем свою операционную систему. Немного теории и начальный загрузчик.


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

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

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

org 0x7C00
	jmp word boot
; Данные начального загрузчика
label disk_id byte at $$
boot_msg db "MyOS boot loader. Version 0.04",13,10,0
reboot_msg db "Press any key...",13,10,0
; Вывод строки DS:SI на экран
write_str:
	push ax si
	mov ah, 0x0E
 @:
	lodsb
	test al, al
	jz @f
	int 0x10
	jmp @b
 @:
	pop si ax
	ret
; Критическая ошибка
error:
	pop si
	call write_str
; Перезагрузка
reboot:
	mov si, reboot_msg
	call write_str
	xor ah, ah
	int 0x16
	jmp 0xFFFF:0
; Точка входа в начальный загрузчик
boot:
	; Настроим сегментные регистры
	jmp 0:@f
 @:
	mov ax, cs
	mov ds, ax
	mov es, ax
	; Настроим стек
	mov ss, ax
	mov sp, $$
	; Разрешим прерывания
	sti
	; Запомним номер загрузочного диска
	mov [disk_id], dl
	; Выводим приветственное сообщение
	mov si, boot_msg
	call write_str
	; Завершение
	jmp reboot
; Пустое пространство и сигнатура
rb 510 - ($ - $$)
db 0x55,0xAA

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

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

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

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

Первая из описанных функций предназначена для вывода текстовых сообщений на экран. Она принимает единственный параметр в паре регистров DS:SI. Строка должна оканчиваться нуль-символом (использование null-terminated строк сейчас является стандартом в подавляющем большинстве операционных систем и языков программирования, и это вполне оправданно - символ с кодом 0 не нужен для простых текстовых данных). Вывод осуществляется с помощью сервиса BIOS.

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

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

call error
db "DISK READ ERROR!",13,10,0

В итоге будет выведено сначала сообщение об ошибке, а потом предложение нажать любую клавишу для перезагрузки. За последнее действие отвечает функция reboot. Она выводит сообщение "Press any key...", а затем ждёт нажатия на любую клавишу (этим занимается нулевая функция прерывания BIOS 0x16). После того, как нажата клавиша, происходит прыжок на точку входа в BIOS - FFFF:0000. Возможно, было бы корректнее вызывать прерывание 0x18, но на многих машинах оно лишь приводит к попытке загрузки со следующего устройства, я же хочу эффект аналогичный нажатию Reset.

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

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

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

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

fasm boot.asm boot.bin

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

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

 

Конфиг для Bochs:

megs: 64
boot: floppy
floppya: 1_44=bin/boot.bios.bin, status=inserted 

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

Заключение

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

Если у вас есть какие-либо вопросы ко мне насчёт содержания этого выпуска или последующих - вы всегда можете написать мне электронное сообщение на адрес kiv.apple@gmail.com

До встречи!


В избранное