27 September 2010

Пишем свою ОС: Выпуск 2

System Programming
Здравствуйте. Это снова мы, iley и pehat, с долгожданной второй статьёй из цикла «Пишем свою ОС» (первая статья здесь). Извиняемся за большую паузу после первой статьи, нам понадобилось некоторое время, чтобы определить дальнейшее направление нашей работы. В этом выпуске мы кратко рассмотрим защищённый режим 32-битных процессоров Intel. Ещё раз подчеркнём, что мы не ставим себе целью дать исчерпывающие теоретические данные.

Давайте на минуту вспомним нашу программу из предыдущего выпуска. Она запускалась вместо операционной системы и выводила сообщение «Hello world». Программа была написана на 16-битном ассемблере и работала в так называемом реальном режиме. Как вы наверняка знаете, при адресации в реальном режиме физический адрес формируется с помощью сегмента и смещения и имеет размерность 20 бит. Несложная математика подсказывает нам, что таким образом можно обращаться всего к мегабайту оперативной памяти.

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

Чтобы решить эти две проблемы, Intel в своё время разработали новый, значительно более сложный способ адресации оперативной памяти. Точнее, Intel разработали даже несколько способов адресации, и все они известны под собирательным названием защищённый режим.

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

В сегментном режиме в памяти выделяются, как ни странно, сегменты. Эти сегменты значительно отличаются от простых и знакомых нам сегментов реального режима. В данном случае сегмент — это непрерывная область памяти, которая характеризуется базой, лимитом (т.е. грубо говоря, размером) и некими дополнительными атрибутами. База — это, грубо говоря, физический адрес, с которого начинается сегмент (на самом деле, это не физический адрес, а так называемый линейный, но об этом позже). Важно заметить, что, в отличие от сегментов реального режима, наши сегменты могут иметь произвольный размер. База, размер и атрибуты каждого сегмента хранятся в дескрипторе. Дескрипторы хранятся в специальных таблицах, а для выбора конкретного дескриптора из таблицы используются селекторы. Не пугайтесь такой кучи новых страшных слов, мы сейчас всё разложим по полочкам.

Итак, при каждом обращении к памяти процессор берёт из регистра (например, CS), селектор, находит по нему нужный дескриптор в таблице, извлекает из дескриптора адрес начала сегмента, прибавляет к адресу начала смещение и получает линейный адрес (обратите внимание, что линейный адрес и физический адрес — это разные вещи, но на данном этапе можно полагать, что это одно и то же). Кроме того, процессор проверяет, не вышел ли полученный адрес за границу сегмента и допускают ли атрибуты сегмента доступ к нему в данный момент. Схема вычисления линейного адреса выглядит примерно так:



Итак, как мы уже говорили, в дескрипторе хранится база (base address), размер (segment limit) и всякие дополнительные атрибуты сегмента. Давайте взглянем на схему дескриптора.



Обратите внимание, что под базу выделено 32 бита, а под лимит всего 20. Как же так, — спросите вы — разве нельзя создать сегмент, размер которого будет больше мегабайта? Можно. Для этого используется несложный трюк. Если бит G установлен в единицу, то лимит считается не в байтах, а в блоках по 4 Кбайт. Надо заметить, что лимит содержит размер сегмента минус один в единицах гранулярности, т.е. если лимит равен 0, то сегмент имеет размер 1 байт или 4 Кбайт. Кроме того, как вы видите, дескриптор включает ещё несколько полей. Их подробное описание можно найти, например, здесь.

Как упоминалось, дескрипторы хранятся в специальных таблицах. Таблиц может быть несколько, но в любом случае в памяти обязана присутствовать GDT — глобальная таблица дескрипторов. Она одна на всю операционную систему. В свою очередь, у каждой задачи, то есть процесса может быть ноль, одна или несколько LDT — локальных таблиц дескрипторов. Адрес и размер GDT хранится в регистре GDTR, текущей LDT — в регистре LDTR. Пока нам LDT не понадобится. Кроме того, бывают еще IDT — таблицы дескрипторов прерываний, но их рассмотрение отложим до следующего выпуска.

Для выбора дескриптора из таблицы используется структура данных, называемая селектором. Вот как он выглядит:



Селектор содержит в себе бит, указывающий, в какой таблице искать дескриптор, локальной или глобальной (L/G) и собственно номер дескриптора (descriptor number). Кроме того, у селектора есть поле RPL, но оно нас пока не интересует.

Итак, давайте перейдём к делу!

;16-битная адресация, пока мы находимся в реальном режиме
use16
org 0x7c00
start:
jmp 0x0000:entry ;теперь CS=0, IP=0x7c00
entry:
mov ax, cs
mov ds, ax

;очистить экран
mov ax, 0x0003
int 0x10

;открыть A20
in al, 0x92
or al, 2
out 0x92, al

;Загрузить адрес и размер GDT в GDTR
lgdt [gdtr]
;Запретить прерывания
cli
;Запретить немаскируемые прерывания
in al, 0x70
or al, 0x80
out 0x70, al

;Переключиться в защищенный режим
mov eax, cr0
or al, 1
mov cr0, eax

;Загрузить в CS:EIP точку входа в защищенный режим
O32 jmp 00001000b:pm_entry

;32-битная адресация
use32
;Точка входа в защищенный режим
pm_entry:
;Загрузить сегментные регистры (кроме SS)
mov ax, cs
mov ds, ax
mov es, ax

mov edi, 0xB8000 ;начало видеопамяти в видеорежиме 0x3
mov esi, msg ;выводимое сообщение
cld
.loop ;цикл вывода сообщения
lodsb ;считываем очередной символ строки
test al, al ;если встретили 0
jz .exit ;прекращаем вывод
stosb ;иначе выводим очередной символ
mov al, 7 ;и его атрибут в видеопамять
stosb
jmp .loop
.exit

jmp $ ;зависаем

msg:
db 'Hello World!', 0

;Глобальная таблица дескрипторов.
;Нулевой дескриптор использовать нельзя!
gdt:
db 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
db 0xFF, 0xFF, 0x00, 0x00, 0x00, 10011010b, 11001111b, 0x00
gdt_size equ $ - gdt

;данные, загружаемые в регистр GDTR
gdtr:
dw gdt_size - 1
dd gdt

finish:
times 0x1FE-finish+start db 0
db 0x55, 0xAA ; сигнатура загрузочного сектора


Теперь немного поясним код.

В строках:
in al, 0x92<br/>
or al, 2<br/>
out 0x92, al

происходит разблокирование адресной линии A20. Что это значит? Вспомним, что в реальном режиме можно адресовать 1 МБ памяти в формате сегмент:смещение (20 бит на адрес). Однако, обратившись, например, по адресу FFFF:FFFF, можно “прыгнуть” немного выше этой планки, и полученный адрес будет иметь длину 21 бит. В процессорах до 80286 старший (двадцатый, если считать от нуля) отбрасывался, и поэтому для совместимости со старыми программами ввели блокировку адресной линии A20. В настоящее время все ОС работают в защищенном режиме, и поэтому для нужд адресации необходимо как можно раньше разблокировать этот бит. Такие дела.

После загрузки адреса и лимита глобальной таблицы дескрипторов в регистр GDTR идет поголовный запрет на прерывания. Объясняется это тем, что все обработчики прерываний работают в реальном режиме, и в защищенном режиме начнутся проблемы с адресацией. Так что пока строго-настрого запрещаем прерывания.

Включение защищенного режима осуществляется путем установки младшего бита регистра CR0. Сразу после перехода в защищенный режим возникает некая неопределенность: в CS:EIP надо установить точку входа в защищенный режим в формате селектор:смещение, а у нас там все еще пережитки реального режима. Поэтому выполняем следующую инструкцию:
O32 jmp 00001000b:pm_entry

Здесь используется префикс преобразования разрядности операнда O32, который позволяет делать дальний безусловный переход с 32-битным смещением. Ура, мы наконец-то можем воспользоваться прелестями защищенного режима!

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

Нам не хватило фантазии на что-нибудь, демонстрирующее реальные возможности защищенного режима, поэтому мы снова выводим “Hello, World!”, но уже пользуясь прямым доступом к видеопамяти. Чтобы сделать что-нибудь красивое, было бы удобно воспользоваться прерываниями. Тому, как использовать их в защищенном режиме, будет посвящена следующая статья нашего цикла. А вторая статья на этом заканчивается. Будем рады увидеть ваши отзывы и пожелания.
Tags:ассемблерсоздание осцикл статейhello worldзащищенный режим
Hubs: System Programming
+94
39.3k 222
Comments 67
Popular right now
Разработчик ОС Linux
from 150,000 to 200,000 ₽ЭвоторМосква
Senior system developer/ С++
to 170,000 ₽GETMOBITМосква
Middle Ruby Developer
from 250,000 ₽WorkatoRemote job
Software Engineer - Research
from 150,000 to 220,000 ₽Droice LabsRemote job