[11] - процесс загрузки
Процесс загрузки.
То, что я до сих пор сделал пока рассчитано только на работы с дисками 1,4Мб, то есть с флопами. Это конечно ограничение в некоторой степени, но пока система еще далеко не готова, этого достаточно. Естественно это еще не окончательный вариант. Да и можно ли говорить об окончательности программных продуктов? Нет предела совершенству.
В обязанности бутсектора входит следующее:
1. Загрузить с диска дополнительные части кода и служебную информацию файловой системы.
2. Загрузить с диска файл сценария (конфигурации) загрузки.
3. Загрузить с диска ядро и модули.
4. Перейти в защищенный режим.
5. Передать управление ядру.
Если с первым и двумя последними пунктами все просто и компактно, то второй и третий пункт требуют возможности работы с файловой системой, а третий пункт помимо этого должен знать структуру бинарных форматов. На все это не хватает 512 байт, отводимых для бутсектора. Наш бутсектор занимает больше - один килобайт.
В файловой системе EXT2 с этим не возникает никаких проблем, поскольку первый килобайт файловой системы не используется.
В FAT это немного сложнее. Служебная структура, именуемая Boot Sector Record (BSR), содержит в себе все необходимые поля для выделения для загрузочного сектора места более чем 512 байт. Но как это сделать при форматировании, стандартными средствами, я не нашел. И если формат диска не соответствует каким-то внутренним представлениям Windows, то содержимое такого нестандартного диска может быть испорчено. Выход был найден случайно. Как оказалось утилита format хоть и не имеет таких параметров командной строки, но перед форматированием берет информацию из BSR. И если предварительно заполнить эту структуру (с нужными нам параметрами), а потом уже форматировать, то все получается так, как хочется нам. Таким образом, у меня получилось сделать диск, у которого два сектора зарезервированы (там будет размещаться boot), и одна копия FAT.
Ну теперь давайте по порядку рассмотрим все этапы работы бутсектора.
Загрузка с диска дополнительной части кода и служебной информации файловой системы.
Бутсектор загружается БИОСом по адресу 0:7c00h занимает он 512 байт. Память начиная с адреса 0:7e00h свободна. но в эту память мы загрузим второй сектор бута. Одновременно загружается информация необходимая для обслуживания файловой системы. Для EXT2 дополнительно необходимо загрузить два килобайта (суперблок и дескрипторы групп), для FAT немного больше - 4,5 килобайта (первая копия FAT).
Код:
mov ax, 0x7e0
mov es, ax
Адрес 0:7e00h идентичен адресу 7e0h:0. Вторым вариантом мы и будем пользоваться, потому что наша процедура загрузки секторов размещает их по сегментному адресу, хранящемуся в es.
В ax номер сектора, с которого начинается чтение (первый сектор является нулевым (каламбур

. И далее все зависит от файловой системы.
Код:
%ifdef EXT2FS
mov cx, 5
Для EXT2 загружается 5 секторов - второй сектор бутсектора (1 сектор), суперблок файловой системы (2 сектора) и дескрипторы групп (2 сектора).
Код:
%elifdef FATFS
mov cx, 10
Для FAT загружается 10 секторов - второй сектор бутсектора (1 сектор), таблица FAT - 9 секторов (такой размер она имеет на floppy дисках).
Код:
%else
%error File system not specified
%endif
call load_block
Все. первый пункт загрузки выполнен.
Функции обслуживания файловых систем имеют одинаковый интерфейс. Cобственно их всего две fs_init и fs_load_file. Естественно у них различаются реализации, но в процессе компиляции выбирается используемая файловая система. Для совместного использования нам никак не хватит одного килобайта, да и не за чем это.
Загрузка с диска файла сценария (конфигурации) загрузки.
Из-за сложности VFAT (FAT с длинными именами) он не реализован. Все имена на диске FAT должна иметь формат 8.3
В файловой системе FAT я не оперирую принятыми в MS системах именами дисков и при указании пути использую путь относительно корневой директории диска (как это делается в юникс системах).
Файл конфигурации у нас пока называется boot.rc и находится в каталоге /etc. Формат у этого файла достаточно нестрогий. Из-за нехватки места в boot секторе там сделана реакция только на ключевые слова, которыми являются:
* kern[el] - файл ядра;
* modu[le] - файл модуля;
* #end - конец файла конфигурации.
Использование этих слов в другом контексте недопустимо.
Предварительно проинициализировав файловую систему
call fs_init
Мы загружаем этот файл с диска.
mov si, boot_config
call fs_load_file
...
boot_config:
db '/etc/boot.rc', 0
Содержимое файла конфигурации такое:
kernel /boot/kernel
#end
Модулей у нас пока никаких нет, да и ядро еще в зачаточном состоянии. Но речь сейчас не об этом.
Загрузка с диска ядра и модулей.
Про этот момент я не буду особо расписывать, желающие могут посмотреть в исходниках, которые в скором времени появятся на сайте.
Скажу только, что программа анализирует файл конфигурации, в соответствии с ключевыми словами загружает ядро (которое может быть в единственном экземпляре) и любое количество модулей. Общий объем ядра и модулей ограничен свободным размером базовой памяти (около 600к).
Перейдем к предпоследнему пункту.
Переход в защищенный режим.
Бутсектор не особо беспокоится об организации памяти в системе - это забота ядра. Для перехода в защищенный режим он описывает всего два сегмента: сегмент кода и сегмент данных. оба сегмента имеют базовый адрес - 0 и предел в 4 гигабайта (это нам пригодиться для проверки наличия памяти).
Перед переходом в защищенный режим нам необходимо включить адресную линию A20. По моим сведениям этот механизм ввели в пору 286 для предотвращения несанкционированных обращений к памяти свыше одного мегабайта (непонятно зачем?). Но поскольку это имеет место быть - нам это нужно обрабатывать, иначе каждый второй мегабайт будет недоступен. Делается это почему-то через контроллер клавиатуры (еще одна загадка).
Код:
mov al, 0xd1
out 0x64, al
mov al, 0xdf
out 0x60, al
После этого можно переходить в защищенный режим.
В регистр gdtr загружается дескриптор GDT.
Очищается регистр флагов.
Код:
mov eax, cr0
or al, 1
mov cr0, eax
Включается защищенный режим. Не смотря на то, что процессор уже находится в защищенном режиме, мы пока работаем в старых адресах. Чтобы от них уйти нам нужно сделать дальный переход в адреса защищенного режима.
Код:
jmp 16:.epm
BITS 32
.epm:
16 в этом адресе перехода - это не сегмент. Это селектор сегмента кода.
Код:
mov ax, 8
mov ds, ax
mov es, ax
; Ставим стек.
mov ss, ax
movzx esp, sp
После всего этого мы инициализируем сегментные регистры соответствующими селекторами, в том числе и сегмент стека, но указатель стека у нас не меняется, только теперь он становится 32-х битным.
Код:
...
gd_table:
; пеpвый дескpиптоp - данные и стек
istruc descriptor
at descriptor.limit_0_15, dw 0xffff
at descriptor.base_0_15, dw 0
at descriptor.base_16_23, db 0
at descriptor.access, db 0x92
at descriptor.limit_16_19_a, db 0xcf
at descriptor.base_24_31, db 0
iend
; втоpой дескpиптоp - код
istruc descriptor
at descriptor.limit_0_15, dw 0xffff
at descriptor.base_0_15, dw 0
at descriptor.base_16_23, db 0
at descriptor.access, db 0x9a ; 0x98
at descriptor.limit_16_19_a, db 0xcf
at descriptor.base_24_31, db 0
iend
Это GDT - Глобальная таблица дескрипторов. Здесь всего два дескриптора, но во избежание ошибок в адресации обычно вводится еще один дескриптор - нулевой, который не считается допустимым для использования. Мы не будем резервировать для него место специально, просто начало таблицы сместим на 8 байт выше.
Код:
gd_desc:
dw 3 * descriptor_size - 1
dd gd_table - descriptor_size
А это содержимое регистра GDTR. Здесь устанавливается предел и базовый адрес дескриптора. обратите внимание на базовый адрес, здесь происходит резервирование нулевого дескриптора.
Теперь процессор находится в защищенном режиме и уже не оперирует сегментами, а оперирует селекторами. Селекторов у нас всего три. Нулевой - недопустим. восьмой является селектором данных и шестнадцатый - селектором кода.
После этого управление можно передать ядру. дальше со всем этим будет разбираться оно.
Передача управления ядру.
Здесь вообще все просто. Когда мы загрузили ядро, в файле ядра мы определили адреса сегмента кода и сегмента данных. Не смотря на то, что ядро имеет вполне конкретные смещения в сегменте (которые задаются при компиляции), код инициализации ядра рассчитан на работу без привязки к адресам. Это нужно для определения количества памяти, после перевода ядра на свои адреса доступ ко всей памяти будет для ядра затруднен в связи с включением механизма страничного преобразования.
Итак, переходим к выполнению кода ядра.
Код:
mov ebx, kernel_data
mov eax, [ebx + module_struct.code_start]
jmp eax
В этом фрагменте в eax записывается адрес начала кодового сегмента ядра.
Так как сегмент кода у нас занимает всю виртуальную память, нам не важно где находится ядро (хотя мы знаем, что оно было загружено в базовую память). Мы просто передаем ему управление.