Памяти Intel 8086 посвящается... [встаёт, выпрямляется, снимает шляпу, пускает слезу, поёт печально и протяжно] Я тебя-я-я никогда-а-а не забу-у-уду, Я тебя-я-я никогда-а-а не уви-и-ижу... (стихи - Андрей Вознесенский)
Для большинства прикладных программистов основные преимуществами защищённого режима (из тех, о которых они слышали…) это значительное увеличение объёма адресуемой памяти и возможность практически отказаться от сегментации. В частности, именно о такой модели памяти в 199?-x годах мечтал один мой знакомый, писавший аналог DOOM-а для MS-DOS.
Системные программисты видят ситуацию с несколько другой стороны. Погрузившись в защищённый режим глубже, они понимают, что сегменты размером 4 Гб и плоский (flat) режим адресации это, конечно, замечательно и очень полезно с практической точки зрения, но для ОС это далеко не главное. Куда важнее то, что памятью теперь действительно можно управлять.
ПРИМЕЧАНИЕ В реальном режиме всё управление памятью сводилось к записи значения в сегментный регистр, ничего большего нельзя было сделать в принципе. В защищённом режиме возможности гораздо шире (в частности, можно организовать тот самый плоский режим). |
Ну а, в качестве естественной компенсации, в защищённом режиме управлять памятью не только можно, но и необходимо, и, конечно же, это выливается в весьма ощутимые дополнительные усилия, как при изучении, так и при кодировании.
Управление памятью в защищённом режиме разбивается на две почти независимые части:
Необходимой является только сегментация, и именно она рассмотрена в этой главе.
Перед тем, как переходить к собственно управлению памятью, надо выяснить, что такое «память», и в каком виде она доступна программисту.
ПРИМЕЧАНИЕ Скорее всего, вы знаете ответы на эти вопросы для реального режима, но в защищённом режиме будет немного иначе. Привыкайте, в нём почти всё немного иначе. |
Мой адрес – не дом и не улица, мой адрес Советский Союз!
В защищённом режиме существует три различных типа адресов и три соответствующих им адресных пространства (address space):
Логическим адресом называется пара <сегмент>:<смещение>, на практике встречающаяся в виде <сегментный регистр>:<смещение>. Так как разрядность сегментного регистра 16, а смещения 32 бита, теоретически, таким образом можно было бы адресовать 248 байт (256 Тб). Но из-за некоторых особенностей сегментных регистров защищённого режима, это число уменьшается до 246 байт (64 Тб), что, конечно, тоже немало.
«Промежуточный» адрес, вводит дополнительный уровень абстракции, именно благодаря его существованию возможно использование страничной адресации (подробнее – в главе про виртуальную память). 32 разряда, плоское (несегментированное) адресное пространство размером 4 Гб.
Физический адрес это число, выставляемое процессором на шину адреса. Разрядность зависит от модели процессора:
Фактически, это множество адресов, к которым может обращаться процессор.
ПРИМЕЧАНИЕ Физический адрес не обязательно является адресом в ОЗУ. Это может быть адрес в ПЗУ, в видеопамяти, регистр какого-либо устройства, адрес которому ничего не соответствует... Куда относится конкретный физический адрес зависит от «железа», причём далеко не только от процессора. |
Поскольку программист использует логические адреса, а процессор физические, необходимо преобразование. Оно происходит в два этапа: логический – линейный и линейный – физический. Первый этап выглядит так:
<линейный адрес> = <базовый адрес сегмента> + <смещение>
ПРИМЕЧАНИЕ Обращаю внимание: сегменты могут перекрываться. Хуже того, скорее всего, они будут перекрываться. А как иначе вместить 64 логических терабайта в 4 линейных гигабайта? |
Если страничная адресация не используется (наш случай), второй этап получается вырожденным:
<физический адрес> = <линейный адрес>
ПРИМЕЧАНИЕ Таким образом, в отсутствии страничной адресации, даже при 36-ти битной шине адреса невозможно обратиться к памяти по адресам старше 4 Гб. Зато всё гораздо проще… |
Для программиста, сегмент это адресуемый блок памяти с заданными свойствами. Подходя к вопросу чуть более формально, можно сказать, что сегмент описывается следующими параметрами:
ПРИМЕЧАНИЕ Сегмент – логическое понятие. Физически есть: - Некая структура, описывающая свойства сегмента (про неё ниже в этой же главе). Очевидно, что это не сегмент. - Часть линейного адресного пространства, на которую проецируется сегмент. Во-первых, она не обладает никакими «заданными свойствами», во-вторых, на любую часть линейного адресного пространства могут проецироваться несколько сегментов. Поэтому и это не сегмент. - Процессор, который связывает первое и второе. Естественно, он тоже не сегмент. |
Как уже было сказано, логический адрес <сегмент>:<смещение> преобразуется процессором в линейный адрес по формуле:
<линейный адрес> = <базовый адрес сегмента> + <смещение>
Поскольку смещение 32-х разрядное, таким образом можно адресовать область линейного адресного пространства, начинающуюся от базового адреса (base address) сегмента и имеющую размер 4 Гб. Но допустимые смещения ограничиваются вторым параметром – размером сегмента (segment size). Процессор считает, что к сегменту относится непрерывная область линейного адресного пространства, начинающаяся с базового адреса сегмента и содержащая количество байт, равное его размеру. Логические адреса со смещением, равным или превосходящим размер сегмента, выходят за пределы сегмента, обращение к таким адресам приводит к исключению #GP (General Protection Fault; подробнее об исключениях в соответствующей главе).
ПРИМЕЧАНИЕ Размер сегмента имеет тот же смысл, что и размер массива в C. Например, размер массива char array[100]; равен 100, но последний элемент массива – array[99]. Так же и с сегментами: смещение последнего адресуемого байта это размер сегмента – 1. |
Взаимосвязь между всеми этими понятиями продемонстрирована на Рис.1.
ПРИМЕЧАНИЕ Помимо обычных сегментов существуют expand down (переводится как «растущий вниз») сегменты, для которых ограничения задаются иначе. Их полезность в реальных программах представляется мне сомнительной, тем не менее, expand down сегменты кратко описаны в приложении. |
Свойства сегмента определяют его тип и правила использования (попытка использовать сегмент не по правилам обычно приводит к исключению #GP). Не вдаваясь в детали, можно выделить следующие типы сегментов:
Как видите, в списке нет сегментов стека. Вообще-то, специально для этого предназначены упомянутые выше expand down сегменты, но с точки зрения классификации они являются частным случаем сегмента данных. Кроме того, к сожалению, они обладают тремя недостатками:
К счастью, использование expand down сегментов не обязательно, сегментом стека может быть любой доступный для чтения и записи сегмент данных.
Слово состоит из трёх частей. Если в исходном слове после первой части вставить пробел, получится французская фамилия. Вторая часть это звук, с которым приходит понимание сути целого слова. Третья часть - название геометрической формы бублика. А всё вместе это структура, описывающая некоторую системную сущность. Шарада.
Логически, дескриптор (descriptor) это структура, описывающая некую системную сущность. В частности, дескриптор сегмента (segment descriptor; сегментный дескриптор) описывает сегмент. Физически дескриптор занимает восемь байт и имеет вполне определённый формат, зависящий от типа дескриптора.
ПРИМЕЧАНИЕ Форматы дескрипторов могут показаться вам несколько странными и даже нелогичными. Но будьте снисходительны к разработчикам, учтите, что им приходилось соблюдать обратную совместимость с 16-ти разрядным 80286. В 80286 дескрипторы занимали те же восемь байт, но вся «полезная нагрузка» была сосредоточена в шести младших байтах, а старшие два были зарезервированы на будущее. Вот в них-то и пришлось уложить всю 32-х разрядность. |
Формат дескриптора сегмента кода/данных (code or data segment descriptor, это официальное название) таков:
ПРИМЕЧАНИЕ Напоминаю, что в борьбе точности и грамотности победила точность. Биты и байты считаются от нуля, то есть нулевой, первый, второй и т.п. |
Положение | Название | Краткое описание |
---|---|---|
Два младших байта (нулевой и первый) | Segment Limit (part 1) | Младшие 16 бит 20-ти битного поля Segment Limit. Поле Segment Limit используется для вычисления размера сегмента, содержит номер последнего блока (блоки нумеруются от 0, размер блока определяет флаг G, см. ниже), являющегося частью сегмента. Алгоритм вычисления размера сегмента подробно описан ниже. |
Второй, третий, четвёртый байты | Base Address (part 1) | Младшие три байта 32-х битного поля Base Address. Поле Base Address содержит базовый адрес сегмента в линейном адресном пространстве. |
0-й бит пятого байта | ?? | Пока неважно, устанавливайте в 0. |
1-й бит пятого байта | R/W | Для сегмента кода называется R (Read enable), для сегмента данных – W (Write enable). В случае сегмента кода управляет возможностью чтения его содержимого, в случае сегмента данных управляет возможностью модификации. Если флаг установлен, то можно, если нет, то нельзя. |
2-й бит пятого байта | ?? | Пока неважно, устанавливайте в 0. |
3-й бит пятого байта | Code/Data | Если флаг установлен, дескриптор описывает сегмента кода, если сброшен – сегмент данных. |
4-й – 7-й биты пятого байта | ?? | Пока неважно, устанавливайте в 1001b. |
0-й – 3-й биты шестого байта | Segment Limit (part 2) | Старшие 4 бита поля Segment Limit. |
4-й – 6-й биты шестого байта | ?? | Пока неважно, устанавливайте в 0. |
7-й бит шестого байта | G | Granularity. Флаг гранулярности. Используется для вычисления размера сегмента, определяет, в каких единицах он указан. Если флаг сброшен, размер сегмента указан в байтах, если установлен – в 4096-ти байтных блоках (4096 == 1000h). |
Седьмой байт | Base Address (part 2) | Старший байт поля Base Address. |
Графически это выглядит так:
И обещанный алгоритм вычисления размера сегмента (на языке «псевдо-С»):
<количество блоков> = <Segment Limit> + 1; if (G == 1) // Если флаг гранулярнсти установлен { <размер блока> = 4096; } else { <размер блока> = 1; } <размер сегмента> = <размер блока> * <количество блоков>; |
В результате:
ПРЕДУПРЕЖДЕНИЕ Вся эта арифметика относится только к размеру сегмента! Базовый адрес сегмента всегда совпадает со значением поля Base Address. |
ПРЕДУПРЕЖДЕНИЕ Относится ко всем встречающимся в курсе структурам, в том числе к дескрипторам! Поля, занимающие больше одного байта, находятся в стандартном для Intel x86 формате Little Endian, то есть в байте с младшим адресом расположена менее значимая часть числа (например, число 12345678h будет представлено последовательностью байт 78h 56h 34h 12h). Применительно к дескриптору сегмента кода/данных, это означает следующее: - в нулевом байте – младшие 8 разрядов Segment Limit, в первом – от 8-го до 15-го. - во втором байте – младшие 8 разрядов Base Address, в третьем – от 8-го до 15-го, в четвёртом – от 16-го до 23-го. |
Дескриптор сегмента данных, размер сегмента 4 Гб, базовый адрес 0, Read/Write:
db 0FFh ; Segment Limit db 0FFh db 0 ; base address db 0 db 0 db 10010010b ; 1001, C/D – 0, 0, R/W – 1, 0 db 10001111b ; G - 1, 000, Limit - 1111 db 0 ; base address |
Дескриптор сегмента данных, размер сегмента 64 Кб, базовый адрес 0, Read/Write:
db 0FFh ; Segment Limit db 0FFh db 0 ; base address db 0 db 0 db 10010010b ; 1001, C/D – 0, 0, R/W – 1, 0 db 00000000b ; G - 0, 000, Limit - 0000 db 0 ; base address |
Дескриптор сегмента кода, размер сегмента 64 Кб, базовый адрес 12345678h, Execute/Read:
db 0FFh ; Segment Limit db 0FFh db 78h ; base address db 56h db 34h db 10011010b ; 1001, C/D – 1, 0, R/W – 1, 0 db 00000000b ; G - 0, 000, Limit - 0000 db 12h ; base address |
Как видите, определять дескрипторы таким образом вполне возможно и даже не слишком сложно. Но всё-таки, когда основной принцип уже понятен, удобнее использовать вот такую структуру:
segment_descriptor struct limit_low dw 0 ; Младшие два байта поля Segment limit base_low dw 0 ; Младшие два байта поля Base Address base_high0 db 0 ; Второй байт поля Base Address type_and_permit db 0 ; Флаги. Этот байт отвечает за то, кто и как может ; использовать дескриптор. Подробнее – в следующих ; главах. flags db 0 ; Ещё одни флаги base_high1 db 0 ; Старший байт поля Base Address segment_descriptor ends |
Если внимательно посмотреть на примеры дескрипторов, приведённые выше, несложно заметить, что это всего лишь объявления некоторых восьмибайтных структур. Но просто объявить в своей программе такую структуру и мысленно назвать её дескриптором совершенно недостаточно для того, чтобы начать использовать описываемый этим дескриптором сегмент. Потому что процессор читать мысли не умеет и задумку не поймёт... Для того чтобы процессор понял, дескриптор должен находиться в глобальной таблице дескрипторов (Global Descriptor Table, GDT).
ПРИМЕЧАНИЕ А почему бы вместо этого не увеличить размер сегментных регистров до восьми байт, чтобы туда влезал дескриптор? Об этом ниже, в разделе «Селекторы и сегментные регистры». |
Таблица дескрипторов состоит из двух частей:
Рассмотрим по частям.
Формат области памяти – массив дескрипторов, в котором нулевой дескриптор начинается со смещения 0, первый – 8, второй – 16 и т.п. Подобный массив, содержащий 3 дескриптора, мог бы выглядеть так (дескрипторы взяты из примера выше):
; сегмент данных, 4 Гб, базовый адрес 0, Read/Write segment_descriptor <0ffffh, 0, 0, 10010010b, 10001111b, 0> ; сегмент данных, 64 Кб, базовый адрес 0, Read/Write segment_descriptor <0ffffh, 0, 0, 10010010b, 0, 0> ; сегмент кода, 64 Кб, базовый адрес 12345678h, Execute/Read segment_descriptor <0ffffh, 5678h, 34h, 10011010b, 0, 12h> |
Местоположение и размер GDT задаёт регистр GDTR (очевидно, от GDT Register; это не ключевое слово ассемблера, а условное название, которое применяется в документации для обозначения этого регистра), вот описание его формата:
Положение | Описание |
---|---|
Два младших байта | Смещение последнего байта таблицы дескрипторов. То есть размер таблицы в байтах минус 1. Так как дескрипторов должно быть целое число, размер должен делиться на 8 (размер дескриптора в байтах), а смещение последнего байта должно быть равно 8n-1. |
Оставшиеся четыре байта | Линейный базовый адрес таблицы дескрипторов. |
Для загрузки регистра GDTR существует специальная команда lgdt
lgdt pointer_to_new_gdtr |
Здесь pointer_to_new_gdtr это указатель на шестибайтную структуру, повторяющую формат GDTR.
Инициализация GDT может выглядеть так:
… ; Вычисляем линейный адрес начала массива дескрипторов mov eax, 0 mov ax, ds shl eax, 4 add eax, offset GDT ; Записываем его в структуру mov dword ptr gdtr + 2, eax ; Загружаем GDTR. ; fword ptr – указатель на шестибайтную структуру lgdt fword ptr gdtr … ; Global Descriptor Table GDT label byte db … ; Дескриптор #0 … db … ; Дескриптор #N gdt_len equ $ - GDT ; размер GDT gdtr dw gdt_len – 1 ; 16-битный размер GDT – 1 dd ? ; Место для 32-х битного базового адреса GDT … |
Опять же, для удобства можно определить структуру:
table_register struct limit dw 0 ; Table Limit base dd 0 ; Linear Base Address table_register ends |
Слово состоит из двух частей. Если между первой и второй частью вставить пробел, полученное словосочетание будет означать «это преподаватель». Всё вместе это структура, используемая для ссылки на дескриптор. Шарада.
В реальном режиме сегменты похожи друг на друга как братья-близнецы, и для описания конкретного сегмента не нужно никаких «структур данных», достаточно знать адрес его начала, а для обращения к сегменту достаточно просто записать этот адрес в какой-нибудь сегментный регистр. Одним из следствий (это может быть как плюс, так и минус) такого подхода является то, что любое приложение достаточно легко получает доступ на чтение, запись или исполнение к любой области памяти, не зависимо от желания ОС или других приложений.
Защищённый режим позволяет разумной ОС более-менее контролировать этот процесс:
ПРИМЕЧАНИЕ Резонное возражение: но ведь пользовательские приложения могут сами вызвать lgtr и создать собственную, «альтернативную» GDT, через которую они получат доступ к любому участку памяти! К счастью, разработчики процессора не забыли о такой возможности, нормальное приложение не сможет сделать ничего подобного, так как для того чтобы успешно использовать инструкцию lgdt оно должно уже взломать защиту ОС каким-то другим способом. Подробнее этот вопрос будет описан в главе, посвящённой защите. Ещё несколько не менее резонных возражений обсуждается в конце главы. |
Селектором называется 16-ти битная структура, используемая для ссылки на находящийся в GDT дескриптор. Формат селектора:
Положение | Название | Описание |
---|---|---|
0-й – 2-й биты | ?? | Пока неважно, устанавливайте в 0. |
3-й – 15-й биты | Index | Номер дескриптора в GDT. |
ПРИМЕЧАНИЕ Как вы помните, максимальное смещение последнего байта, которое можно записать в GDTR это FFFFh, поэтому максимальный размер GDT – 216. А, так как один дескриптор занимает 8 байт, GDT может содержать не более 213 дескрипторов, то есть как раз столько, сколько влезает в поле Index селектора. |
Примеры:
8 ; 0..01000 – первый дескриптор GDT 16 ; 0..10000 – второй дескриптор GDT 0 ; 0..00000 – нулевой дескриптор, см. про него ниже |
ПРИМЕЧАНИЕ Поскольку селектор ссылается на дескриптор, а уже дескриптор описывает сегмент, наверное, правильно было бы говорить «селектор дескриптора сегмента хххх» или даже «селектор, ссылающийся на дескриптор, описывающий сегмент хххх». Но вместо этого я буду употреблять просто «селектор сегмента ххх». Потому что так короче и понятнее… |
Как уже было сказано выше, при работе в защищённом режиме, процессор интерпретирует значения сегментных регистров как селекторы, а не как адреса начала сегментов. Но это не единственная новость :)
Начиная с 80286, сегментные регистры состоят из двух частей: видимой и скрытой (visible part, hidden part). Видимая часть это доступный для чтения/записи 16-ти разрядный регистр, скрытая часть недоступна никак, её наличие можно определить только по некоторым косвенным признакам и документации. Содержимое видимой части зависит от режима работы процессора, а вот в скрытой части регистра в обоих режимах находится некоторый аналог дескриптора, полностью описывающий сегмент. Работает это примерно так (для защищённого режима):
В результате поиск дескриптора, его чтение, и проверка его корректности выполняется всего один раз, что значительно ускоряет обращения к памяти. Но появляются некоторые побочные эффекты:
Второй «побочный эффект» нуждается в нескольких комментариях:
ПРЕДУПРЕЖДЕНИЕ Повторюсь, это недокументированная особенность, теоретически поведение процессора может измениться (на данный момент – Pentium 4 – полёт нормальный, всё осталось, как было), поэтому в реальных программах её использования следует избегать. |
Есть несколько простых правил совместного использования селекторов и сегментных регистров, соблюдение которых убережёт вас от лишних исключений #GP и связанных с ними неприятностей.
Селектор, указывающий на нулевой дескриптор GDT, отличается от прочих. Он имеет три особенности:
Предназначение нулевого селектора – явно «пометить» сегментные регистры, использование которых не планируется. Если этого не сделать, случайное использование таких сегментных регистров может привести к непонятным и плохо воспроизводимым ошибкам.
ПРИМЕЧАНИЕ Побочный эффект – реальное содержимое нулевого дескриптора GDT никогда не используется, так как к нему невозможно обратиться. |
Селектор, загруженный в SS, должен быть селектором сегмента данных, доступного для чтения/записи.
Селектор, загруженный в CS, должен быть селектором сегмента кода.
Селектор, загруженный в любой из этих регистров, должен быть либо селектором сегмента данных, либо селектором сегмента кода, доступного для чтения, либо нулевым селектором.
- Каждому селектору по дескриптору! - Ура!!! - Каждому дескриптору по сегменту! - Ура!!! - Каждому сегменту по мегабайту собственной памяти! - Ура!!! ... Из популистского выступления разработчиков ОС на собрании процессоров.
Итак, к данному моменту рассмотрены все основные понятия и артефакты, относящиеся к сегментации, осталось выяснить только две вопроса:
Иголка в яйце, яйцо в утке, утка в зайце.. К. Бессмертный, «Современные системы безопасности», V в. н.э.
Для начала рассмотрим «концептуально чистый» случай. Пусть на входе логический адрес в виде пары <селектор>:<смещение>, а на выходе нужно получить соответствующий линейный адрес. Алгоритм работы процессора:
Нарисовать это можно так:
Более «правильный» алгоритм должен учитывать то, что селекторы «в свободном виде» встречаются только в командах загрузки адреса (lds, les, lfs, lgs, lss) и дальнего перехода, в остальных же случаях используется селектор, загруженный в сегментный регистр. А, поскольку во время загрузки сегментного регистра процессор уже проверил корректность селектора, нашёл соответствующий дескриптор и сохранил его в скрытой части сегментного регистра, то есть, фактически выполнил шаги (1), (2) и (3), алгоритм можно начинать сразу с (4).
ПРИМЕЧАНИЕ При изучении схемы получения линейного адреса и сравнении её с «целями», описанными в первой главе, возникают естественные вопросы: Где же здесь раздельные адресные пространства? Где отделение кода/данных ОС от кода/данных пользователя? Да, пользователь не может обращаться куда угодно, но всё, что есть в GDT – в его распоряжении! Вопросы хорошие, полные ответы на них можно получить, изучив курс полностью, но чтобы вы не мучались до последней главы, кратко отвечу сейчас. Для разделения адресных пространств пользовательских приложений Intel предлагает использовать два механизма: локальную таблицу дескрипторов (Local Descriptor Table, LDT) и страничную адресацию. LDT не очень интересна и в курсе не рассматривается, а вот страничная адресация обязательно будет описана, но позже, в соответствующей главе. Отделение кода/данных ОС от кода/данных пользователя реализуется при помощи одного из «пока неважных» полей дескриптора. Это поле и предоставляемые им возможности подробно рассмотрены в главе «Теоретическое введение в защиту». |
С учётом существования дескрипторов сегментов и GDT, можно усложнить процедуру переключения режимов, немного приближаясь к эталону и, соответственно, получая больше возможностей.
Переключение из реального режима в защищённый:
Обратное переключение:
Приведённые ниже примеры выполняют эти требования по частям: первый пример демонстрирует работу с сегментом данных, второй – с сегментом кода.
Как известно, процессор 8086 имел 20-ти разрядную шину адреса, поэтому мог адресовать ровно 1 Мб памяти. 80286 имел уже 24-х битную шину адреса, что, теоретически, позволяло ему обращаться к 16 Мб памяти. Но, по замыслу разработчиков, вся эта «роскошь» должна была быть доступна программисту только из защищённого режима, в реальном режиме 80286 должен был полностью повторять 8086. Однако разработчики 80286 не учли, что механизм адресации памяти реального режима (<адрес> = <16-ти битный адрес сегмента> * 16 + <16-ти битный адрес смещения>) позволяет адресовать несколько больше, чем 1 Мб. Например, так:
FFFFh * 16 + 11h = FFFF0h + 11h = 100001h
В подобной ситуации 8086 будет обращаться к младшим байтам всё того же первого мегабайта, а 80286 – к младшим байтам второго. Отличие вызвано тем, что 8086 при всём желании не может выставить на свою 20-ти разрядную адресную шину 1 в 20-ом разряде (они тоже нумеруются от 0), а 80286 не только может, но и выставляет.
Программисты всего мира с восторгом приветствовали эту ошибку: ещё бы, почти 64 Кб дополнительной оперативной памяти! И, когда разработчики процессора спохватились, было поздно – ошибка уже широко использовалась. В результате ошибка стала «особенностью» (it is not a bug, it is a feature), а для полной совместимости с 8086 в процессор был добавлен вентиль A20 (gate A20), принудительно обнуляющий 20-й бит шины адреса.
По умолчанию вентиль A20 закрыт, поэтому перед работой с адресами старше 100000h его нужно открыть. Именно для этого предназначена следующая функция:
; Открывает вентиль A20 open_A20: in al, 92h or al, 2 out 92h, al ret |
Простая программка, переводящая процессор в защищенный режим, загружающая FS селектором 4-х гигабайтного сегмента данных и переключающаяся обратно в реальный режим.
; fs4gb.asm ; Программа, устанавливающая размер сегмента, адерсуемого регистром FS, 4 Гб .model tiny .code .386p org 100h ;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ; Структуры ; ;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Сегментный дескриптор segment_descriptor struct limit_low dw 0 ; Младшие два байта поля Segment limit base_low dw 0 ; Младшие два байта поля Base Address base_high0 db 0 ; Второй байт поля Base Address type_and_permit db 0 ; Флаги flags db 0 ; Ещё одни флаги base_high1 db 0 ; Старший байт поля Base Address segment_descriptor ends ; Регистр, описывающий GDT table_register struct limit dw 0 ; Table Limit base dd 0 ; Linear Base Address table_register ends ;;;;;;;;;;;;;;;;;;;;;;;;;; ; ; Код ; ;;;;;;;;;;;;;;;;;;;;;;;;;; start: ; Подготавливаем DS push cs pop ds ; Открываем вентиль A20 call open_A20 ; Запрещаем прерывания call disable_interrupts ; Инициалиируем GDT call initialize_gdt ; Переключаем режим call set_PE ; загрузить новый селектор в регистр FS mov ax, 8 mov fs, ax call clear_PE call enable_interrupts ret ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; Данные ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Глобальная таблица дескрипторов GDT label byte ; Нулевой дескриптор segment_descriptor <> ; Дескриптор сегмента данных, базовый адрес 0, размер 4Gb, Read-Write segment_descriptor <0ffffh, 0, 0, 10010010b, 10001111b, 0> ; 10010010b - 1001, C/D - 0, 0, R/W - 1, 0 ; 10001111b - G - 1, 000, Limit - 1111 ; Данные для загрузки в GDTR gdtr table_register <$ - GDT - 1, 0> ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; Служебные функции ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Открывает вентиль A20 open_A20: in al, 92h or al, 2 out 92h, al ret ; Инициализирует GDT initialize_gdt: ; Вычисляем линейный адрес начала массива дескрипторов call cs_to_eax add eax, offset GDT ; Записываем его в структуру mov dword ptr gdtr.base, eax ; Загружаем GDTR lgdt fword ptr gdtr ret ; Запрещает маскируемые и немаскируемые прерывания disable_interrupts: cli ; запретить прерывания in al, 70h ; индексный порт CMOS or al, 80h ; установка бита 7 в нем запрещает NMI out 70h, al ret ; Разрешает маскируемые и немаскируемые прерывания enable_interrupts: in al, 70h ; индексный порт CMOS and al, 7Fh ; сброс бита 7 отменяет блокирование NMI out 70h, al sti ; разрешить прерывания ret ; Устанавливает флаг PE set_PE: mov eax, cr0 ; прочитать регистр CR0 or al, 1 ; установить бит PE, mov cr0, eax ; с этого момента мы в защищенном режиме ret ; Сбрасывает флаг PE clear_PE: mov eax, cr0 ; прочитать CR0 and al, 0FEh ; сбросить бит PE mov cr0, eax ; с этого момента мы в реальном режиме ret ; Вычисляет линейный адрес начала сегмента кода cs_to_eax: mov eax, 0 mov ax, cs shl eax, 4 ret end start |
После выполнения этой программы можно делать, например, такие вещи:
… mov eax, 10000000 ; десять мегабайт mov word ptr fs:[eax], 1234h … |
И оно будет работать! Примерно это и называется unreal-режимом.
С сегментом кода есть одна проблема: после переключения, выполнение должно продолжаться с того адреса, с которого нужно, а не с того, с которого получится. Проиллюстрировать проблему может следующий кусок псевдокода:
… call disable_interrupts call set_PE ; Дальний переход. После него в CS должен оказаться ; селектор первого дескриптора GDT, то есть 8, ; а выполнение должно родолжиться с метки next_command_PM. jmp <8> : ???? next_command_PM: … ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; Данные ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Глобальная таблица дескрипторов GDT label byte ; Нулевой дескриптор segment_descriptor <> ; Дескриптор сегмента кода, размер 4 Gb ; Адрес начала - ???? segment_descriptor <0ffffh, ?, ?, 10010010b, 10001111b, ?> … |
Поскольку программа загружается DOS по произвольному адресу, адрес next_command_PM не известен на этапе компиляции, и мы не можем просто прописать какие-то константы в качестве базового адреса сегмента и смещения. Есть два решения:
В примерах продемонстрированы оба варианта, первый в «практичном» примере, второй – в «пафосном».
ПРИМЕЧАНИЕ Ещё один вариант – писать exe-программу, в этом случае загрузчик DOS самостоятельно пропишет вместо имени сегмента адрес его начала. Но, поскольку exe-программы несколько сложнее, да и не должен разработчик ОС рассчитывать на присутствие в памяти DOS и его умного загрузчика, этот вариант не рассматривается. |
Кроме того, после возврата в реальный режим необходимо выполнить дальний переход, перезаписывающий CS, иначе в CS останется селектор сегмента, а не адрес. На первый взгляд, ничего страшного в этом нет, но при первом же прерывании содержимое CS будет сохранено в стеке, а при возврате из прерывания оно будет интерпретироваться уже не как селектор, а как адрес сегмента. И, естественно, возврат произойдёт не туда, куда хотелось бы.
Ограничение на размер сегмента данных уже успешно преодолено, осталось повторить это для сегмента кода.
ПРИМЕЧАНИЕ Совместив эту программу с предыдущей, то есть, установив всем сегментам кода/данных базовый адрес в 0 и размер 4 Gb, можно получить «почти тот самый» плоский режим, используемый современными ОС. Чтобы получить «совсем тот самый» 32-разрядный плоский режим, надо прочитать в приложение о 32-х разрядных сегментах кода. |
; cs4gb.asm ; Программа, устанавливающая размер сегмента кода 4 Gb, а потом обратно 64 Kb .model tiny .code .386p org 100h ;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ; Структуры ; ;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Сегментный дескриптор segment_descriptor struct limit_low dw 0 ; Младшие два байта поля Segment limit base_low dw 0 ; Младшие два байта поля Base Address base_high0 db 0 ; Второй байт поля Base Address type_and_permit db 0 ; Флаги flags db 0 ; Ещё одни флаги base_high1 db 0 ; Старший байт поля Base Address segment_descriptor ends ; Регистр, описывающий GDT table_register struct limit dw 0 ; Table Limit base dd 0 ; Linear Base Address table_register ends ;;;;;;;;;;;;;;;;;;;;;;;;;; ; ; Код ; ;;;;;;;;;;;;;;;;;;;;;;;;;; start: ; Подготавливаем DS push cs pop ds ; Вычисляем и устанавливаем правильное смещение в long-jmp-to-PM call cs_to_eax add eax, offset next_command_PM mov cs:pm_offs, eax ; Устанавливаем правильный сегмент в long-jmp-to-RM mov ax, cs mov cs:rm_cs, ax ; Прописываем адрес начала cs в качестве базового адреса сегмента call cs_to_eax mov dsc64kb.base_low, ax shr eax, 16 mov dsc64kb.base_high0, al ; Открываем вентиль A20 call open_A20 ; Запрещаем прерывания call disable_interrupts ; Инициализируем GDT call initialize_gdt ; Переключаем режим call set_PE ; 32-разрядный дальний переход. Перключает содержимое cs из нормального ; для реального режима (адрес) в нормальное для защищённого (селектор). ; 32-разрядность нужна для потому, что смещение может занимать ; больше 16-ти разрядов. ; Базовый адрес целевого сегмента 0, поэтому смещение вычисляется и ; записывается время выполнения db 66h ; префикс изменения разрядности операнда db 0EAh ; код команды дальнего перехода pm_offs dd 0 ; смещение dw 8 ; селектор next_command_PM: ; В данный момент сегмент кода - 4 Гб, базовый адрес 0 ; 16-разрядный дальний переход. Переключает сегмент кода с ; сегмента размером 4 Гб на сегмент размеров 64 Кб. ; Базовый адрес сегмента вычисляется во время выполнения, ; поэтому смещение можно прописать заранее db 0EAh ; код команды дальнего перехода dw $ + 4 ; смещение dw 16 ; селектор ; В данный момент сегмент кода - 64 Кб, базовый адрес равен ; адресу сегмента кода до переключения в защищённый режим call clear_PE ; Мы в реальном режиме, осталось разобраться с ; значением регистра cs ; 16- разрядный дальний переход. Перключает содержимое cs из нормального ; для защищённог режима (селектор) в нормальное для реальног (адрес). ; Адрес сегмента вычисляется и прописывается во время выполнения. db 0EAh ; код команды дальнего перехода dw $ + 4 ; смещение rm_cs dw 0 ; сегмент call enable_interrupts ret ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; Данные ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Глобальная таблица дескрипторов GDT label byte ; Нулевой дескриптор segment_descriptor <> ; Дескриптор сегмента кода, размер 4 Gb segment_descriptor <0ffffh, 0, 0, 10011010b, 10001111b, 0> ; 10011010b - 1001, C/D - 1, 0, R/W - 1, 0 ; 10001111b - G - 1, 000, Limit - 1111 ; Дескриптор сегмента кода, размер 64 Kb dsc64kb segment_descriptor <0ffffh, 0, 0, 10011010b, 0, 0> ; 10011010b - 1001, C/D - 1, 0, R/W - 1, 0 ; 0 - G - 0, 000, Limit - 0 ; Данные для загрузки в GDTR gdtr table_register <$ - GDT - 1, 0> ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; Служебные функции ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ... ; Служебные функции – те же, что и ранее end start |
4 Гб это, конечно, здорово, но, поскольку на протяжении курса мы не будем писать таких больших программ, нам нигде не потребуется сегмент кода размером более 64 Кб. Поэтому переключение сегмента кода можно упростить:
ПРИМЕЧАНИЕ В приведённой ниже реализации, сегмент кода устанавливается практически таким же, какой был в реальном режиме, то есть базовые адреса совпадают, размер 64 Кб. Помимо того, что в результате упрощается переключение, имеется ещё один приятный побочный эффект: смещения команд при переключении сегментов останутся неизменными, а значит, вычисленные компоновщиком адреса останутся правильными. Это тоже упрощает программу. |
; cs64kb.asm ; Программа, переключающаяся в защищённый режим ; и устанавливающая сегмента кода такой же ка был в реальном .model tiny .code .386p org 100h ;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ; Структуры ; ;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Сегментный дескриптор segment_descriptor struct limit_low dw 0 ; Младшие два байта поля Segment limit base_low dw 0 ; Младшие два байта поля Base Address base_high0 db 0 ; Второй байт поля Base Address type_and_permit db 0 ; Флаги flags db 0 ; Ещё одни флаги base_high1 db 0 ; Старший байт поля Base Address segment_descriptor ends ; Регистр, описывающий GDT table_register struct limit dw 0 ; Table Limit base dd 0 ; Linear Base Address table_register ends ;;;;;;;;;;;;;;;;;;;;;;;;;; ; ; Код ; ;;;;;;;;;;;;;;;;;;;;;;;;;; start: ; Подготавливаем DS push cs pop ds ; Устанавливаем правильный сегмент в long-jmp-to-RM mov ax, cs mov cs:rm_cs, ax ; Прописываем адрес начала cs в качестве базового адреса сегментов call cs_to_eax mov dsc64kb.base_low, ax shr eax, 16 mov dsc64kb.base_high0, al ; Запрещаем прерывания call disable_interrupts ; Инициализируем GDT call initialize_gdt ; Переключаем режим call set_PE ; 16-разрядный дальний переход. Перключает содержимое cs из нормального ; для реального режима (адрес) в нормальное для защищённого (селектор). ; Базовый адрес целевого сегмента совпадает с cs, ; поэтому смещение можно прописать сразу db 0EAh ; код команды дальнего перехода dw $ + 4 ; смещение dw 8 ; селектор ; В данный момент сегмент кода - 64 Кб, базовый адрес равен ; адресу сегмента кода до переключения в защищённый режим, ; потому можно без проблем переключаться обратно call clear_PE ; Мы в реальном режиме, осталось разобраться с ; значением регистра cs ; 16-разрядный дальний переход. Перключает содержимое cs из нормального ; для защищённог режима (селектор) в нормальное для реальног (адрес). ; Адрес сегмента вычисляется и прописывается во время выполнения. db 0EAh ; код команды дальнего перехода dw $ + 4 ; смещение rm_cs dw 0 ; сегмент call enable_interrupts ret ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; Данные ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Глобальная таблица дескрипторов GDT label byte ; Нулевой дескриптор segment_descriptor <> ; Дескриптор сегмента кода, размер 64 Kb dsc64kb segment_descriptor <0ffffh, 0, 0, 10011010b, 0, 0> ; 10011010b - 1001, C/D - 1, 0, R/W - 1, 0 ; 0 - G - 0, 000, Limit - 0 ; Данные для загрузки в GDTR gdtr table_register <$ - GDT - 1, 0> ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; Служебные функции ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ... ; Служебные функции – те же, что и ранее end start |