Процесс загрузки

Процесс загрузки.

В обязанности бутсектора входит следующее:

  1. Загрузить с диска дополнительные части кода и служебную информацию файловой системы.
  2. Загрузить с диска файл сценария (конфигурации) загрузки.
  3. Загрузить с диска ядро и модули.
  4. Перейти в защищенный режим.
  5. Передать управление ядру.

Если с первым и двумя последними пунктами все просто и компактно, то второй и третий пункт требуют возможности работы с файловой системой, а третий пункт помимо этого должен знать структуру бинарных форматов. На все это не хватает 512 байт, отводимых для бутсектора. Наш бутсектор занимает больше - один килобайт.
В файловой системе EXT2 с этим не возникает никаких проблем, поскольку первый килобайт файловой системы не используется.
В FAT это немного сложнее. Служебная структура, именуемая Boot Sector Record (BSR), содержит в себе все необходимые поля для выделения для загрузочного сектора места более чем 512 байт. Но как это сделать при форматировании, стандартными средствами, я не нашел. И если формат диска не соответствует каким-то внутренним представлениям Windows, то содержимое такого нестандартного диска может быть испорчено. Выход был найден случайно. Как оказалось утилита format хоть и не имеет таких параметров командной строки, но перед форматированием берет информацию из BSR. И если предварительно заполнить эту структуру (с нужными нам параметрами), а потом уже форматировать, то все получается так, как хочется нам.

Ну теперь давайте по порядку рассмотрим все этапы работы бутсектора.

Загрузка с диска дополнительной части кода и служебной информации файловой системы.

Бутсектор загружается БИОСом по адресу 0:7c00h занимает он 512 байт. Память начиная с адреса 0:7e00h свободна. но в эту память мы загрузим второй сектор бута. Одновременно загружается информация необходимая для обслуживания файловой системы. Для EXT2 дополнительно необходимо загрузить два килобайта (суперблок и дескрипторы групп), для FAT немного больше - 4,5 килобайта (первая копия FAT).

        mov ax, 0x7e0
        mov es, ax

Адрес 0:7e00h идентичен адресу 7e0h:0. Вторым вариантом мы и будем пользоваться, потому что наша процедура загрузки секторов размещает их по сегментному адресу, хранящемуся в es.

        mov ax, 1

В 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

После этого можно переходить в защищенный режим.

        lgdt [gd_desc]

В регистр gdtr загружается дескриптор GDT.

        push byte 2
        popf

Очищается регистр флагов.

        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 записывается адрес начала кодового сегмента ядра.
Так как сегмент кода у нас занимает всю виртуальную память, нам не важно где находится ядро (хотя мы знаем, что оно было загружено в базовую память). Мы просто передаем ему управление.



Опубликовал admin
24 Апр, Суббота 2004г.



Программирование для чайников.