ОСдев №10: основной загрузчик, часть 3. GDT.
Подготовительный этап закончен, можно приступать к интересному. Кроме собственно загрузки файлов наша программа должна подготовить для ОС рабочую среду. Это значит: переключить процессор в 32/64-битный режим, настроить таблицу прерываний и создать базовую GDT. Сегодня разберёмся с последним пунктом.
GDT - сокращение от Global Descriptor Table, глобальной таблицы дескрипторов. Что это такое? По сути - набор записей одинакового формата, описывающих области памяти и разрешения, которые они имеют. Упрощённо это выглядит так:
//область 0
адрес области
размер области
параметры доступа
флаги
//область 1
адрес области
размер области
параметры доступа
флаги
...
В 32/64-битном режиме дескрипторные таблицы используются вместо старой схемы адресации сегмент:смещение. Зачем? Сегмент:смещение - небезопасная технология, которая позволяет переписать любой участок памяти. Надо ли говорить, что при неосторожном обращении это легко может закончиться бедой? Дескрипторная таблица даёт возможность ограничить запись или выполнение кода в отдельных областях RAM. Дескрипторные таблицы появились как часть аппаратной защиты памяти вместе с 286 процессором.
Как это работает? После выхода из 16-битного режима процессор больше не принимает адреса в формате сегмент:смещение. Если попытаетесь так сделать - получите исключение GPT (General Protection Fault). Вместо этого в сегментном регистре процессор ожидает получить смещение дескриптора внутри ДТ. При этом для операций над этим сегментом будут действовать правила, указанные в дескрипторе. Например, попытка обратиться к памяти за пределами сегмента или запись в защищённый от записи сегмент будут вызывать исключения (кстати, про обработку исключений поговорим позже, пока давайте примем, что это фатальная ошибка, которая приведёт к остановке программы).
Кроме глобальной таблицы дескрипторов существуют ещё локальные (ЛДТ), TSS и таблицы дескрипторов прерываний (IDT). Для того, чтобы наша ОС могла начать работу, обязательно наличие только двух таблиц: GDT и IDT. Давайте теперь взглянем на GDT поподробнее. Скажу сразу, зрелище будет не очень приятное. Но начнём с лёгкого. Так как GDT - часть аппаратной схемы защиты памяти, у неё есть свой регистр: GDTR. Это 48-битный регистр, 4 байта которого предназначены для смещения GDT, а 2 - для её размера. Таким образом, GDT не может быть больше 65536 байтов в размере. Размер записи в GDT - 8 байтов, значит,< таблица может иметь максимум 8192 дескриптора. Зная всё это, хорошим тоном было бы сразу зарезервировать 64К под GDT, но в моей архитектуре ОС создаёт свои таблицы, так что сейчас я обойдусь минимумом. Минимум в данном случае - 3 дескриптора. Нулевой, сегмент кода и сегмент данных. Зачем отдельно выделять нулевой дескриптор? Дело в том, что обращение к нему в GDT приводит к, вы угадали, исключению. Это тоже своего рода мера предосторожности.
А теперь время взглянуть на структуру дескриптора. И тут, увы, наследие тяжёлого прошлого во всей красе. Ради обратной совместимости в кодом для старых процессоров дескриптор GDT превратили в кашу.
Первые два байта - это первые 16 битов границы сегмента.
Следующие три байта - первые 24 бита основания сегмента.
Следующий байт - параметры доступа. Рассмотрим ниже.
Следующий байт совмещает в себе биты 16-19 границы и флаги. Об этом тоже подробнее ниже.
Ну и последний байт - биты 24-31 основания.
Неудобно? Не то слово. Когда будем писать ядро - обязательно замутим процедуру для комфортной работы с этим месивом. К счастью, сейчас у нас статичная структура всего из трёх сегментов, так что заполнить можно и вручную. Создадим и подключим модуль GDT.inc. Как это сделать, мы рассматривали в прошлый раз. И добавим в него вот такую таблицу:
Это и есть наша GDT, ничего ужасного. Значения в нулевом дескрипторе для нас не важны, а вот остальные давайте рассмотрим подробнее. У нас есть два дескриптора: один - для кода, другой - для данных. Оба начинаются с 0 и занимают FFFFF*4Kib = 4Gib. Фактически это значит, что, начав работать, ОС сможет использовать всю память по своему усмотрению. Давайте теперь разберём параметры доступа и флаги.
Таким образом, значение параметров доступа 10010010b даёт нам вот что: это сегмент данных, запись в него разрешена, сегмент растёт вверх, уровень привилегий - ring0. А теперь флаги. Биты 0-3 здесь заняты границей сегмента, не обращаем на них внимания.
Окей, теперь у нас есть GDT. Но как указать системе, что её нужно использовать? Процессор ведь не дурак, сам искать не станет. Всё просто, джентльмены из IBM в кои-то веки о нас позаботились. При помощи специальной ассемблерной команды lgdt (load GDT) мы можем передать в регистр GDTR линейный адрес таблицы и её размер. Для этого добавим перед GDT такую структуру:
Размер таблицы мы уже знаем, а вот адрес придётся посчитать, так что пока оставим 0 и напишем процедуру инициализации GDT:
На случай, если тут не всё очевидно, поясню. Мы помещаем в EAX смещение GDT относительно сегмента, а потом добавляем адрес сегмента*16. Это и есть линейный адрес, сохраняем его в структуре. После этого отключаем прерывания, передаём структуру процессору командой lgdt и включаем прерывания обратно. По идее прерывания можно не трогать, так как в 16-битном режиме GDT не используется, но я перестраховщик.
Собственно, на этом всё. Добавьте вызов init_GDT в конец загрузчика перед cli и дело в шляпе. Сегодня без картинки, но вот вам котик.
Чистая дискета: https://drive.google.com/file/d/1Bold4ds8oEruHQ7fJZKHglVo7A2Vc5MR/view?Предыдущие части:
Отличный комментарий!