Программирование процессоров Intel x86 в защищённом режиме
Управление памятью: сегменты и дескрипторы

Автор: sergh
The RSDN Group

Версия текста: 1.0

Ликбез
Адреса
Сегменты
Дескрипторы сегментов кода/данных
Примеры
Глобальная таблица дескрипторов
Область памяти
Регистр
Селекторы и сегментные регистры
Селекторы
Сегментные регистры
Совместное существование
Всё вместе
Получение линейного адреса
Переключение режимов: инициализация системы управления памятью
Вентиль A20
Примеры
Сегмент данных размером 4 Гб
Сегмент кода
Задания

Памяти 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.


Рисунок 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.
Таблица 1. Формат дескриптора сегмента кода/данных.

Графически это выглядит так:


Рисунок 2. Формат дескриптора сегмента кода/данных.

И обещанный алгоритм вычисления размера сегмента (на языке «псевдо-С»):

<количество блоков> = <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.
Оставшиеся четыре байта Линейный базовый адрес таблицы дескрипторов.
Таблица 2. Регистр GDTR


Рисунок 3. Регистр GDTR

Для загрузки регистра 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; Дескриптор #0db; Дескриптор #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.
Таблица 3. Формат селектора.


Рисунок 4. Формат селектора

ПРИМЕЧАНИЕ

Как вы помните, максимальное смещение последнего байта, которое можно записать в 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

Селектор, загруженный в SS, должен быть селектором сегмента данных, доступного для чтения/записи.

Регистр CS

Селектор, загруженный в CS, должен быть селектором сегмента кода.

Регистры DS, ES, FS, GS

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

Всё вместе

- Каждому селектору по дескриптору!
- Ура!!!
- Каждому дескриптору по сегменту!
- Ура!!!
- Каждому сегменту по мегабайту собственной памяти!
- Ура!!!
...
Из популистского выступления разработчиков ОС
на собрании процессоров.

Итак, к данному моменту рассмотрены все основные понятия и артефакты, относящиеся к сегментации, осталось выяснить только две вопроса:

Получение линейного адреса

Иголка в яйце, яйцо в утке, утка в зайце..

К. Бессмертный, «Современные системы безопасности», V в. н.э.

Для начала рассмотрим «концептуально чистый» случай. Пусть на входе логический адрес в виде пары <селектор>:<смещение>, а на выходе нужно получить соответствующий линейный адрес. Алгоритм работы процессора:

  1. Если хранящийся в <селекторе> индекс дескриптора выходит за границы GDT (определяются по содержимому GDTR), выбрасывается исключение.
  2. По адресу начала GDT (расположен в GDTR) и индексу дескриптора определяется адрес начала дескриптора, дескриптор считывается.
  3. Анализируется дескриптор и характер запроса с принципиальной точки зрения. На этом этапе отсекаются попытки использовать дескриптор способами, для которых он не предназначен, например, исполнять сегмент данных или читать/писать нечитаемый сегмент кода. Если проверка не проходится, генерируется исключение. Это уровень загрузки селектора в сегментный регистр: пока что не важно, как именно будет применяться этот регистр, есть только имя регистра и селектор.
  4. Анализируется дескриптор и характер запроса с конкретной точки зрения. Если по каким-то причинам обращение невозможно (например, попытка записи в Read-Only сегмент или выход <смещения> за пределы сегмента) генерируется исключение.
  5. Из дескриптора извлекается базовый адрес сегмента и складывается со <смещением>.

Нарисовать это можно так:


Рисунок 5. Алгоритм получения линейного адреса.

Более «правильный» алгоритм должен учитывать то, что селекторы «в свободном виде» встречаются только в командах загрузки адреса (lds, les, lfs, lgs, lss) и дальнего перехода, в остальных же случаях используется селектор, загруженный в сегментный регистр. А, поскольку во время загрузки сегментного регистра процессор уже проверил корректность селектора, нашёл соответствующий дескриптор и сохранил его в скрытой части сегментного регистра, то есть, фактически выполнил шаги (1), (2) и (3), алгоритм можно начинать сразу с (4).

ПРИМЕЧАНИЕ

При изучении схемы получения линейного адреса и сравнении её с «целями», описанными в первой главе, возникают естественные вопросы: Где же здесь раздельные адресные пространства? Где отделение кода/данных ОС от кода/данных пользователя? Да, пользователь не может обращаться куда угодно, но всё, что есть в GDT – в его распоряжении!

Вопросы хорошие, полные ответы на них можно получить, изучив курс полностью, но чтобы вы не мучались до последней главы, кратко отвечу сейчас.

Для разделения адресных пространств пользовательских приложений Intel предлагает использовать два механизма: локальную таблицу дескрипторов (Local Descriptor Table, LDT) и страничную адресацию. LDT не очень интересна и в курсе не рассматривается, а вот страничная адресация обязательно будет описана, но позже, в соответствующей главе.

Отделение кода/данных ОС от кода/данных пользователя реализуется при помощи одного из «пока неважных» полей дескриптора. Это поле и предоставляемые им возможности подробно рассмотрены в главе «Теоретическое введение в защиту».

Переключение режимов: инициализация системы управления памятью

С учётом существования дескрипторов сегментов и GDT, можно усложнить процедуру переключения режимов, немного приближаясь к эталону и, соответственно, получая больше возможностей.

Переключение из реального режима в защищённый:

  1. Запретить маскируемые и немаскируемые прерывания.
  2. Инициализировать GDT и загрузить её адрес в GDTR.
  3. Установить флаг PE (младший бит регистра CR0).
  4. Выполнить дальний переход (jmp или call) для перезагрузки регистра CS.
  5. Перезагрузить все сегментные регистры.

Обратное переключение:

  1. Сделать текущим сегментом кода доступный для чтения сегмент с пределом FFFFh байт.
  2. Загрузить во все сегментные регистры селекторы дескрипторов доступных для записи сегментов данных с пределом FFFFh.
  3. Сбросить флаг PE
  4. Выполнить дальний переход (jmp или call) для перезагрузки регистра CS.
  5. Перезагрузить сегментные регистры.
  6. Разрешить прерывания.

Приведённые ниже примеры выполняют эти требования по частям: первый пример демонстрирует работу с сегментом данных, второй – с сегментом кода.

Вентиль A20

Как известно, процессор 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

Примеры

Сегмент данных размером 4 Гб

Простая программка, переводящая процессор в защищенный режим, загружающая 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 будет сохранено в стеке, а при возврате из прерывания оно будет интерпретироваться уже не как селектор, а как адрес сегмента. И, естественно, возврат произойдёт не туда, куда хотелось бы.

[Пафосный вариант] Сегмент кода 4 Гб

Ограничение на размер сегмента данных уже успешно преодолено, осталось повторить это для сегмента кода.

ПРИМЕЧАНИЕ

Совместив эту программу с предыдущей, то есть, установив всем сегментам кода/данных базовый адрес в 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 

[Практичный вариант] Сегмент кода 64 Кб

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 

Задания


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.