Программирование процессоров Intel x86 в защищённом режиме
Защита: передача управления

Автор: sergh
The RSDN Group

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

[Первая сторона] Что мы знаем о защищённой ОС
Структура
Схема работы
Поправки к модели
Что нужно такой ОС от механизма передачи управления
[Вторая сторона] Классификация способов передачи управления
Объединяем
Требование (4)
Требование (2)
Требование (3)
Требование (5)
Требование (6)
Итого
[Первая сторона] Передача управления через шлюз
[Вторая сторона] Переключение стека
Туда (call, int, исключение, внешнее прерывание)
Обратно (retf, iret, iretd)
All together now
Туда-и-обратно
Переключение режимов: инициализация TSS
Использование RPL
Пример
Изменение уровня привилегий
Задания

На границе тучи ходят хмуро,
Край суровый тишиной объят. 
У высоких берегов Амура 
Часовые Родины стоят 

Борис Ласкин

Время перейти эту реку вброд,
Самое время перейти эту реку вброд

Борис Гребенщиков

Время пришло! Это замковая глава, она объединяет все предыдущие, сводит в цельную работающую систему и закрывает большую часть «хвостов».

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

Не огорчайтесь, если вам придётся прочитать эту главу пару раз. Представьте себе, сколько времени я её писал и сколько раз за это время перечитывал соответствующие места [Intel 2004] :)

[Первая сторона] Что мы знаем о защищённой ОС

Немного подумав над главой «Теоретическое введение в защиту», а заодно и над всеми предыдущими, можно построить упрощённую модель гипотетической защищённой ОС.

Структура

Приблизительная структура ОС изображёна на Рисунке 1 (красным обозначены дескрипторы GDT и IDT, к которым приложение не имеет доступа). Правда, пока неясно, зачем нужен Синий Сегмент Состояния Задачи, но интуиция уже подсказывает, что без него тут не обойтись…


Рисунок 1. Структура защищённой ОС.

Более-менее то же самое, с небольшими поясняющими комментариями, описывает Таблица 1.

Дескриптор… DPL Результат
… сегмента, содержащего код ядра ОС. 0 Ядро ОС имеет все необходимые ему привилегии.
… сегмента, содержащего стек ядра ОС. 0 К стеку имеет доступ только ядро.
… сегмента, содержащего данные ядра ОС. 0 К данным имеет доступ только ядро.
… сегмента, содержащего код пользовательского приложения. 3 Приложение не имеет никаких лишних возможностей.
… сегмента, содержащего стек пользовательского приложения. 3 В качестве стека такой сегмент может использоваться только пользовательским приложением. Но поскольку любой стек это, прежде всего, сегмент данных, ядро тоже имеет к нему доступ.
… сегмента, содержащего данные пользовательского приложения. 3 Приложение имеет доступ к данным. И ядро, при необходимости, тоже.
… шлюза вызова, запрещённого для использования из пользовательских приложений (на картинке нет). 0 При попытке приложения выполнить инструкцию call с соответствующим селектором, будет сгенерировано исключение #GP.
… шлюза вызова, разрешённого для использования из пользовательских приложений (на картинке нет). 3 Приложения могут использовать соответствующие системные функции.
… шлюза ловушки/прерывания, запрещённого для «явного» вызова из пользовательских приложений. 0 При попытке приложения выполнить инструкцию int n с соответствующим номером, будет сгенерировано исключение #GP. Относится так же к инструкциям int 3 и into. На обработку аппаратных прерываний и исключений этот механизм не влияет, они будут обработаны независимо от уровня DPL.
… шлюза ловушки/прерывания, разрешённого для «явного» вызова из пользовательских приложений. 3 Приложения могут использовать соответствующие прерывания.
… сегмента состояния задачи. Любой Об этом ниже.
Таблица 1. Структура защищённой ОС.

Схема работы

Схема работы такой ОС достаточно очевидна (многозадачности нет):

Во время работы приложения могут генерироваться исключения, происходить внешние прерывания, вызываться программные прерывания. Всё это отражено на Рисунке 2. Во время работы ядра тоже могут генерироваться исключения, происходить внешние прерывания и вызываться программные прерывания, это на рисунок не попало. Обращения к системным функциям через шлюзы вызова также не попали на рисунок.


Рисунок 2. Схема работы защищённой ОС

Поправки к модели

Существенные упрощения, внесённые в модель сознательно:

Несущественные упрощения, по разным причинам проникшие в модель или в её изображение:

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

Что нужно такой ОС от механизма передачи управления

Наконец, добрались до того, ради чего эта модель строилась. Глядя на схему работы, по пунктам:

Теперь подумаем о программистах, писавших ядро и приложение:

И, наконец, вспомним о безопасности:

Иначе систему будет слишком просто, как минимум, уронить, как очень вероятный максимум – получить для своего кода нулевой уровень привилегий.

[Вторая сторона] Классификация способов передачи управления

Вас просто так послать, или по факсу?

Владимир Вишневский

По инструкциям/событиям:

ПРИМЕЧАНИЕ

Обычно инструкции ret/retf/iret/iretd используются для возврата из функции или обработчика прерывания. Во-первых, естественно, это тоже «передача управления». Во-вторых, предварительно подготовив стек, эти инструкции можно использовать гораздо шире.

Существует ещё один, глубоко противоестественный, способ передачи управления, использующий страничную адресацию. При этом стандартная защита отдыхает, но в данном случае это не проблема, так как без нулевого уровня привилегий всё равно работать не будет. Идея способа поражает воображение, но практическая применимость сильно ограничена, подробнее – в главе «Страничная адресация».

По целевому сегменту кода:

ПРИМЕЧАНИЕ

Если рассматривать подчиненные сегменты кода, добавится ещё один пункт.

По участвующим дескрипторам:

Объединяем

Попытаемся наложить требования защищённой ОС на рассмотренные выше варианты передачи управления. В таблице 2 перечислены все основные требования ОС, а в соседнем столбце дана расшифровка, приближенная к реалиям процессоров Intel x86.

ПРИМЕЧАНИЕ

Напоминаю: чем больше DPL, тем меньше привилегий. Соответственно, сегмент кода с большим DPL менее привилегирован, чем сегмент кода с меньшим DPL. Сегменты кода ядра имеют наименьший DPL, сегменты кода пользовательского приложения – наибольший.

# Требования Что это означает на практике
1 При запуске ОС из кода ядра можно передать управление коду пользовательского режима. Существует способ передачи управления от сегмента кода с меньшим DPL к сегменту кода с большим DPL. Не важно какой :)
2 При вызове системной функции, возникновении исключения, программного или аппаратного прерывания управление передаётся обработчику, находящемуся в ядре. Через шлюз можно передать управление из сегмента кода с большим DPL в сегмент кода с меньшим DPL (из приложения в ядро) и между сегментами кода с одинаковым DPL (из ядра в ядро).
3 При возврате из системной функции, обработчика исключения, программного или аппаратного прерывания управление передаётся в точку вызова или на прерванную инструкцию. Из сегмента кода с меньшим DPL, можно передать управление в сегмент кода с большим DPL инструкциями iretd и retf. И между сегментами кода с одинаковым DPL – тоже.
4 Внутри себя приложение может передавать управление без ограничений. Внутри себя ядро может передавать управление без ограничений. Ближняя передача управления (внутри сегмента кода) разрешена всегда, любыми способами. Передача управления напрямую (без дескрипторов шлюза) между сегментами кода с одинаковым уровнем привилегий разрешена всегда, любыми способами.
5 Код пользовательского режима может вызывать только явно предназначенные для этого функции кода ядра, но не может передать управление в произвольно выбранное место кода ядра. Из сегмента кода с большим DPL, невозможно напрямую передать управление в сегмент кода с меньшим DPL.
6 Проверки на доступ к сегменту происходят при загрузке сегментного регистра. Код пользовательского режима не должен иметь возможности получить доступ к данным режима ядра через сегментные регистры, значения которых были загружены в режиме ядра При изменении уровня привилегий в сторону уменьшения (в сторону увеличения DPL), сбрасываются значения сегментных регистров, которые не могли быть получены на новом уровне привилегий.
7 Код ядра и код пользовательского режима должны пользоваться различными стеками, при передаче управления должно происходить переключение стека. Код с различным уровнем привилегий должен использовать разные сегменты стека, при передаче управления с изменением уровня привилегий стек должен переключаться. Как это реализуется – разберёмся чуть позже.
Таблица 2. Требования и их практическое значение

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

Требование (4)

Это требование очерчивает область, которой защита вообще не касается: передачи управления внутри приложения/ядра – их личное дело, больше они никого волновать не должны. Тем более что при этом не происходит изменения уровня привилегий, то есть ничего значимого с точки зрения защиты.

Таким образом, всегда разрешены:

Требование (2)

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

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

Из главы «Теоретическое введение в защиту» уже известно, что:

Именно через шлюзы налаживается связь между пользовательским кодом и кодом ОС.

Требование (3)

После обработки прерывания/исключения необходимо вернуться назад. Для x86 это означает возможность прямой передачи управления от более привилегированного сегмента кода к менее привилегированному инструкциями iretd и retf. Причём передачи в любую точку, не обязательно к месту вызова, достаточно изменить в стеке адрес возврата.

Это, кстати, и есть способ, который был нужен в Требовании (1).

Требование (5)

Соображения безопасности запрещают прямую передачу из менее привилегированного сегмента в более привилегированный – только через шлюз. Помимо прочего, запрещена прямая передача управления инструкциями iret, iretd и retf. То есть, если вызвать менее привилегированный код через шлюз (например, если поместить в менее привилегированный сегмент кода обработчик прерывания), обработчик не сможет стандартным способом вернуть управление.

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

Требование (6)

Изменение уровня привилегий в сторону уменьшения привилегий – возврат из обработчика исключения/прерывания инструкцией iretd и из системной функции инструкцией retf. При этом система проверяет регистры DS, ES, FS, GS, те из них, которые содержат селекторы слишком привилегированных сегментов (то есть сегментов, DPL которых меньше нового CPL), сбрасываются.

Итого

И спросила кроха: «Что такое хорошо, и что такое плохо?»

Самуил Яковлевич Маршак

Введём обозначения:

С этими обозначениями, получаем следующую табличку «можно/нельзя»:

ПРИМЕЧАНИЕ

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

Способ передачи управления Соотношения уровней привилегий Вердикт Соотношения DPL
Любой В одном сегменте, шлюз не используется можно --
Любой C == T, шлюз не используется можно CPL == DPL(T)
call, jmp C != T, шлюз не используется #GP CPL != DPL(T)
retf, iretd, iret C > T, шлюз не используется можно CPL < DPL(T)
retf, iretd, iret C < T, шлюз не используется #GP CPL > DPL(T)
jmp C == T, C >= G можно CPL == DPL(T), CPL <= DPL(G)
jmp C == T, C < G #GP CPL == DPL(T), CPL > DPL(G)
jmp C != T, независимо от G #GP CPL != DPL(T)
call, int n C <= Т, C >= G можно CPL >= DPL(T), CPL <= DPL(G)
call, int n C <= T, C < G #GP CPL >= DPL(T), CPL > DPL(G)
call, int n C > T, независимо от G #GP CPL < DPL(T)
Исключение C <= T, независимо от G можно CPL >= DPL(T)
Исключение C > T, независимо от G #GP CPL < DPL(T)
Внешнее прерывание C <= T, независимо от G можно CPL >= DPL(T)
Внешнее прерывание C > T, независимо от G #GP CPL < DPL(T)
Таблица 3. «Можно/нельзя».

Что не попало в таблицу:

Несмотря на несколько «умозрительный» способ построения таблицы, она удивительным образом совпала с правильным вариантом :) Единственное, в ней не упомянуты RPL, которые иногда всё-таки оказываются важны.

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

[Первая сторона] Передача управления через шлюз

- Каждому шлюзу – 10-и битную защиту!
- Ура!!!

Митинг продолжается :)

Для начала рассмотрим шлюз вызова. С точки зрения защиты, в процессе участвуют следующие параметры:

  1. Текущий уровень привилегий (CPL)
  2. RPL селектора в инструкции call
  3. DPL шлюза
  4. RPL селектора из дескриптора шлюза
  5. DPL целевого сегмента кода

Все они обозначены и пронумерованы на Рисунке 3.


Рисунок 3. Параметры защиты, участвующие в передаче управления через шлюз вызова

Условия успешной передачи управления (все условия должны выполняться одновременно):

То есть, текущий уровень привилегий (с учётом RPL) должен оказаться достаточным для того, чтобы воспользоваться шлюзом, но при этом он (без учёта RPL) не должен превосходить уровень привилегий кода вызываемой функции. Если что-то не так – исключение #GP.

А как же параметр (4) – RPL селектора из дескриптора шлюза? А он просто игнорируется :)

ПРИМЕЧАНИЕ

При условии, что дескрипторы создаются системой один раз при инициализации, это довольно разумно. Так как, если этот RPL запрещает вызов, через шлюз передать управление не сможет никто – зачем такой шлюз? Значит, если шлюз есть, передача разрешена, а значит RPL можно не проверять :) Другое дело, если бы дескрипторы генерировались динамически… Но такой подход к проектированию ОС разработчики процессора, видимо, не рассматривали.

Со шлюзами ловушки/прерывания всё точно так же, только проще, так как параметров меньше.

  1. Текущий уровень привилегий (CPL)
  2. DPL шлюза
  3. RPL селектора целевого сегмента из дескриптора шлюза (он опять не понадобится)
  4. DPL целевого сегмента кода

Смотрите рисунок 4.


Рисунок 4. Параметры защиты, участвующие в передаче управления через шлюз ловушки/прерывания

Ну и условия успешной передачи управления (все условия должны выполняться одновременно):

То есть, то же самое, что у шлюза вызова, но без RPL.

[Вторая сторона] Переключение стека

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

Основной вопрос, конечно, где же то «откуда-то», которое содержит данные о подходящем стеке. Тут есть два варианта.

Туда (call, int, исключение, внешнее прерывание)

При передаче управления с повышением уровня привилегий, источником данных о стеке служит сегмент состояния задачи (TSS). В главе «Введение в многозадачность: одна задача» он был описан несколько неподробно, в таблице 4 и на рисунке 5 приведено более детальное описание.

Смещение Содержимое
0
4 4 байта – значение смещения стека нулевого уровня привилегий.
8 2 байта – значение селектора стека нулевого уровня привилегий.
12 4 байта – значение смещения стека первого уровня привилегий.
16 2 байта – значение селектора стека первого уровня привилегий.
20 4 байта – значение смещения стека второго уровня привилегий.
24 2 байта – значение селектора стека второго уровня привилегий.
28…104
Таблица 4. Структура сегмента состояния задачи, второе приближение.


Рисунок 5. Структура сегмента состояния задачи, второе приближение

Описание соответствующей структуры на ассемблере:

; Сегмент состояния задачи
task_state_segment struct
                dd      0
    esp0        dd      0
    ss0         dw      0
                dw      0 
    esp1        dd      0
    ss1         dw      0
                dw      0
    esp2        dd      0
    ss2         dw      0
                db      78 dup (0)
task_state_segment ends

Что ещё нужно сказать:

Наконец, более-менее точный сценарий:

На рисунке 6 показано состояние старого и нового стеков сразу же после вызова функции с двумя параметрами.


Рисунок 6. Два стека (на картинке младшие адреса сверху, т.е. стеки растут вверх).

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

Обратно (retf, iret, iretd)

Передача управления с понижением уровня привилегий – фактически возврат из функции или обработчика, данные о старом стеке, так же как и данные об адресе, извлекаются из стека. Подразумевается, что, либо перед этим была выполнена корректная передача «с повышением», либо стек был подготовлен вручную.

Сценарий:

А подготовка стека вручную выглядит, например, так:

        ; подготовка стека для "возврата" в пользовательский режим
        mov  eax, ss3_sel          ; селектор стека 
        push eax
        mov  eax, stack3_size      ; смещение
        push eax
        mov  eax, cs3_sel          ; селектор сегмента кода
        push eax
        mov  eax, offset user_code ; адрес начала пользовательского кода
        push eax

        ; 32-х разрядный retf. Подробнее о разрядностях – в приложении
        db 66h
        retf
ПРЕДУПРЕЖДЕНИЕ

RPL селектора стека, RPL селектора сегмента кода, DPL сегмента стека и DPL сегмента кода должны совпадать между собой, иначе всё это закончится генерацией исключения #GP.

All together now

Наконец, объединим вместе всё, что нам известно про передачу управления, защиту и стеки.

Туда-и-обратно

Roads go ever ever on
Under cloud and under star,
Yet feet that wandering have gone
Turn at last to home afar.

J.R.R.Tolkien

Рассмотрим законченный сценарий, пусть это будет программное прерывание.

  1. Пользовательский код вызывает int 21h
  2. Проверка попадания номер прерывания в IDT, проверка корректности дескриптора, проверка допустимости вызова с точки зрения безопасности – DPL дескриптора шлюза не меньше CPL. В случае каких-то проблем – исключение #GP.
  3. Из дескриптора извлекается селектор и смещение. Проверяется, что селектор попадает в GDT, что это селектор сегмента кода, что смещение в него попадает. Проверка допустимости вызова с точки зрения безопасности – DPL дескриптора сегмента кода не больше CPL. В случае каких-то проблем – исключение #GP.
  4. Пусть DPL целевого сегмента кода не равен CPL
  5. Процессор считывает из регистра TR селектор TSS. В TSS находит нужный селектор сегмента стека и смещение, проверяет на корректность. Текущие значения SS и ESP на время сохраняются во внутренних регистрах, регистрам SS и ESP присваиваются значения из TSS. В случае каких-либо ошибок на этом этапе, генерируется исключение #TS (Invalid TSS).
  6. В новый стек последовательно копируются: старый SS, старый ESP, EFLAGS, CS, EIP. На каждый регистр уходит по 4 байта.
  7. CS и EIP загружаются новыми значениями, изменяется CPL.
  8. Начинается выполнение обработчика …
  9. Завершается. Обработчик возвращает управление инструкцией iretd.
  10. Проверка корректности CS и EIP из стека: попадает в GDT, соответствует сегменту кода, RPL селектора из стека равен DPL сегмента. В случае каких-то проблем – исключение #GP.
  11. Пусть DPL сохранённого CS по прежнему не равен CPL (при желании, обработчик мог всё там поменять).
  12. Из стека считывается значение ESP и SS, проверяются на корректность (попадание в GDT, соответствие доступному для записи сегменту данных) и на правильность уровней привилегий (должен совпадать с RPL и с DPL сегмента кода). В случае каких-то проблем – исключение #GP.
  13. В регистры CS, EIP, EFLAGS, SS, ESP записываются новые значения, проверяются регистры DS, ES, FS, GS, те из них, которые содержат селекторы сегментов, недоступные на новом уровне привилегий, сбрасываются.

К сожалению, мне не удалось изобразить всю последовательность на картинке, поместилась только работа со стеками. Передача управления при обработке программного прерывания была более-менее рассмотрена в главе «Программные прерывания» и не слишком сильно изменилась с тех пор.


Рисунок 7. Переключение стеков при обработке программного прерывания

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

Переключение режимов: инициализация TSS

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

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

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

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

В общем, если не считать всяких необязательных деталей (страничная адресация и LDT, но они действительно необязательны – если не используются, их можно не инициализировать), это полные, правильные алгоритмы, соответствующие написанному в [Intel 2004]. Не прошло и девяти глав :)

Использование RPL

- Какая от него польза, Мастер?
- Никакой, насколько я знаю. - ... 
(и далее по тексту до конца абзаца)

Урсула Ле Гуин, «Волшебник Земноморья»

В главе «Теоретическое введение в защиту» был рассмотрен единственный разумный способ использования RPL – сознательное понижение уровня привилегий системной функцией при обращении по переданному пользователем указателю. Там же было обещано, что в недалёком будущем будет описан способ, как это проделать технически. Будущее наступило :)

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

Уровень привилегий вызывающего кода можно получить из сохранённого в стеке адреса возврата (конкретно – из RPL регистра CS). А для модификации RPL селектора по приведённым выше правилам предназначена инструкция arpl (аббревиатура для Adjust RPL, то есть «подгонка» или «настройка» RPL):

     arpl   dest, source

Здесь source это селектор с «образцовым» RPL, а dest – селектор, RPL которого будет модифицирован. Если в результате исполнения arpl операнд dest был изменён, устанавливается флаг ZF, если нет – сбрасывается. dest может быть 16-и разрядным регистром или областью памяти, source – всегда 16-и разрядный регистр.

ПРИМЕЧАНИЕ

Инструкция arpl не относится к привилегированным и может вызываться из пользовательского режима. При попытке использовать arpl из реального режима, процессор генерирует исключение #UD – неизвестный код операции.

ПРЕДУПРЕЖДЕНИЕ

С этой инструкцией дело нечисто.

Во-первых, в Microsoft Visual C++ 6.0, 2003, 2005 есть уникальная ошибка: если в исходном коде встречается инструкция arpl x, y, то в бинарном файле она превратится в arpl y, x (незаметно для вас поменяется порядок операндов).

Во-вторых, в VMWare 5.0.0.build-13124 arpl ведёт себя ещё более странно – она не просто меняет второй операнд (видимо, писали на VC), но меняет его неправильно (а это уже самостоятельно привнесённое): вместо изменения младших битов просто копируется значение первого операнда.

Судя по этим ошибкам, на практике инструкция arpl используется чрезвычайно редко.

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

Они подробно описаны в [Intel 2004].

ПРИМЕЧАНИЕ

Скорее всего, эти инструкции применяются примерно так же часто, как arpl, со всеми вытекающими :) Так что если они вам вдруг потребуются, готовьтесь к сюрпризам.

Пример

Изменение уровня привилегий

Что делает программа:

Схема работы изображена на рисунке 8.


Рисунок 8. Схема работы примера.

Как она это делает:

На что ещё стоит обратить внимание (при написании примера мне было сложнее всего найти ошибки именно в этих деталях):

Надеюсь, после таких пояснений, проблем с пониманием примера не возникнет.

; change_level.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

; Дескриптор шлюза
gate_descriptor struct
    offset_low  dw      0    ; Два младших байта поля Offset
    selector    dw      0    ; Поле Segment Selector
    params      db      0    ; Количество параметров в стеке
    type_and_permit db  0    ; Флаги
    offset_high dw      0    ; Старшие байты поля Offset
gate_descriptor ends

; Регистр, описывающий таблицу дескриптров
table_register struct
    limit       dw      0    ; Table Limit
    base        dd      0    ; Linear Base Address
table_register ends

; Сегмент состояния задачи
task_state_segment struct
                dd      0
    esp0        dd      0
    ss0         dw      0
                dw      0
    esp1        dd      0
    ss1         dw      0
                dw      0
    esp2        dd      0
    ss2         dw      0
                db      78 dup (0)
task_state_segment ends

;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Код инициализации и завершения работы 
;
;;;;;;;;;;;;;;;;;;;;;;;;;;

start:
        ; Подготавливаем DS
        push    cs
        pop     ds

        ; Устанавливаем правильный сегмент в long-jmp-to-RM
        mov     ax, cs
        mov     cs:rm_cs, ax

        ; Инициализация сегмента TSS
        mov     eax, offset tss_dsc
        mov     ebx, offset tss_seg
        call    init_descriptor_base

        ; Сегменты кода-данных
        mov     eax, offset cs0_dsc  ; системный код
        mov     ebx, 0
        call    init_descriptor_base
        mov     eax, offset cs3_dsc  ; пользовательский код
        call    init_descriptor_base
        mov     eax, offset ds3_dsc  ; пользовательские данные
        call    init_descriptor_base

        ; Сегмент стека для cs0
        mov     eax, offset ss0_dsc
        mov     ebx, offset stack0_seg
        call    init_descriptor_base

        ; Сегмент стека для cs3
        mov     eax, offset ss3_dsc
        mov     ebx, offset stack3_seg
        call    init_descriptor_base

        ; Сегмент стека для переключения в реальный режим
        mov     eax, offset rmss_dsc
        mov     ebx, 0
        call    init_descriptor_base

        ; Сохраняем старый стек
        mov  old_ss, ss
        mov  old_esp, esp

        ; Запрещаем прерывания
        call disable_interrupts
        ; Инициализируем GDT
        call initialize_gdt
        ; Переключаем режим
        call set_PE

        ; 16-разрядный дальний переход. Перключает содержимое cs из нормального
        ; для реального режима (адрес) в нормальное для защищённого (селектор).
        ; Базовый адрес целевого сегмента совпадает с cs,
        ; поэтому смещение можно прописать сразу
        db      0EAh    ; код команды дальнего перехода
        dw      $ + 4   ; смещение
        dw      cs0_sel ; селектор

        ; загрузить селектор дескриптора TSS в TR
        mov  ax, tss_sel
        ltr  ax

        ; Устанавливаем новый стек
        mov  dx, ss0_sel
        mov  ss, dx
        mov  esp, stack0_size

        ; загружаем селектор сегмента с DPL == 3 в DS. Если этого не сделать,
        ; при смене уровня привилегий значение DS будет сброшено.
        mov ax, ds3_sel
        mov ds, ax

        ; подготовка стека для "возврата" в пользовательский режим
        mov  eax, ss3_sel          ; селектор стека
        push eax
        mov  eax, stack3_size      ; смещение
        push eax
        mov  eax, cs3_sel          ; селектор сегмента кода
        push eax
        mov  eax, offset user_code ; адрес начала пользовательского кода
        push eax

        ; 32-х разрядный retf. Подробнее о разрядностях – в приложении
        db 66h
        retf

back_to_rm:
        ; перед переключением в реальный режим установим 
        ; правильный сегмент стека (размером 64 Кб)
        mov  dx, rmss_sel
        mov  ss, dx
        mov  esp, old_esp

        call clear_PE

        ; Мы в реальном режиме, осталось окончательно разобраться со
        ; стеком, значением регистра cs и остальными сегментными регистрами

        ; 16-разрядный дальний переход. Перключает содержимое cs из нормального
        ; для защищённог режима (селектор) в нормальное для реальног (адрес).
        ; Адрес сегмента вычисляется и прописывается во время выполнения.
        db      0EAh   ; код команды дальнего перехода
        dw      $ + 4  ; смещение
rm_cs   dw      0      ; сегмент

        ; Восстанавливаем стек
        mov  dx, old_ss
        mov  ss, dx
        mov  esp, old_esp

        ; устанавливаем разумные значения сегментным регистрам
        mov  ax, cs
        mov  ds, ax
        mov  es, ax

        call enable_interrupts
        ret

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Пользовательский код
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

user_code:

        ; Параметр в стек
        mov eax, 1
        push eax

        ; вызов
        db 9Ah
        dw 0
        dw call_sel

        ; параметр в стек
        mov eax, 123
        push eax

        ; вызов
        db 9Ah
        dw 0
        dw call_sel

        ; бесконечный цикл. По идее, здесь мы оказаться не должны
        jmp $

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Системная функция
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

; Приниммает один четырёхбайтный параметр. В зависимости от его значения:
;   - 123 – завершает работу программы, выходит в RM
;   - 1   - проходит по экрану, инкрементирует атрибуты всех символов
;   - 2   - проходит по экрану, инкрементирует значения всех символов
;   - иначе – не делает ничего
call_handler:

        ; Состояние стека в данный момент:
        ; [esp]      – eip
        ; [esp + 4]  – cs
        ; [esp + 8]  – параметр
        ; (если была смена привилегий) [esp + 12] – старый esp
        ; (если была смена привилегий) [esp + 16] - старый ss

        ; Помещаем параметр в ebx
        mov ebx, [esp + 8]

        ; Подготовка к работе с экраном
        push   eax
        push   ecx
        push   es

        mov    ax, video_sel  ; Сегмент видеопамяти
        mov    es, ax
        mov    eax, 0         ; Текущий символ
        mov    ecx, 80 * 25   ; Колическтво символов на экране

        ; Выбор ветки функции, которая будет выполняться
        cmp  ebx, 123
        je   value123    ; 123
        cmp  ebx, 1
        je   value1      ; 1
        cmp  ebx, 2
        je   value2      ; 2

        ; непонятное значение параметра – просто выходим из функции
        jmp  exit_handler

value123:
        ; Выходим в RM, завершаем работу
        jmp  back_to_rm

value1:
        ; проходит по экрану, инкрементирует атрибуты всех символов
screen_loop1:

        inc    eax
        inc    byte ptr es:[eax]          ; Меняем атрибут
        inc    eax
        loop   screen_loop1

        jmp    exit_handler


value2:
        ; проходит по экрану, инкрементирует значения всех символов
screen_loop2:
        inc    byte ptr es:[eax]          ; Меняем символ
        inc    eax
        inc    eax
        loop   screen_loop2

exit_handler:

        pop    es
        pop    ecx
        pop    eax

        ; 32-х разрядный выход с очисткой стека
        db 66h
        retf 4

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Данные
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

; Глобальная таблица дескрипторов
GDT          label   byte
             ; Нулевой дескриптор
             segment_descriptor <>
             ; Дескриптор шлюза вызова
             gate_descriptor <call_handler, cs0_sel, 1, 11101100b, 0>
             ; Дескриптор TSS, 104 байта, базовый адрес 0
tss_dsc      segment_descriptor <103, 0, 0, 10001001b, 0,  0>
             ; Дескриптор сегмента кода с системными привелегиями
cs0_dsc      segment_descriptor <0ffffh, 0, 0, 10011010b, 0, 0>
             ; Дескриптор сегмента стека для него
ss0_dsc      segment_descriptor <stack0_size - 1, 0, 0, 10010010b, 0, 0>
             ; Дескриптор сегмента кода с пользовательскими привелегиями
cs3_dsc      segment_descriptor <0ffffh, 0, 0, 11111010b, 0, 0>
             ; Дескриптор сегмента стека для него
ss3_dsc      segment_descriptor <stack3_size - 1, 0, 0, 11110010b, 0, 0>
             ; Дескриптор сегмента данных с пользовательскими привилегиями
ds3_dsc      segment_descriptor <0ffffh, 0, 0, 11110010b, 0, 0>
             ; Дескриптор сегмента видеопамяти, доступен только системе
video_dsc    segment_descriptor <0ffffh, 08000h, 0bh, 10010010b, 0, 0>
             ; Дескриптор сегмента стека
rmss_dsc     segment_descriptor <0ffffh, 0, 0, 10010010b, 0, 0>


; Данные для загрузки в GDTR
gdtr      table_register <$ - GDT - 1, 0>

; Селекторы
call_sel     equ 00001000b    ; селектор шлюза вызова
tss_sel      equ 00010000b    ; селектор TSS
cs0_sel      equ 00011000b    ; сегмент системного кода
ss0_sel      equ 00100000b    ; системный стек
cs3_sel      equ 00101011b    ; сегмент пользовательского кода
ss3_sel      equ 00110011b    ; пользовательский стек
ds3_sel      equ 00111011b    ; пользовательские данные
video_sel    equ 01000000b    ; видеопамять
rmss_sel     equ 01001000b    ; стек для реального режима

; Сегменты стека
stack0_seg    db 128 dup (0)
stack0_size   equ $ - stack0_seg
stack3_seg    db 128 dup (0)
stack3_size   equ $ - stack3_seg

; Сегмент TSS
tss_seg   task_state_segment <0, stack0_size, ss0_sel>

; Место для хранения данных о стеке реального режима
old_ss    dw 0
old_esp   dd 0

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Служебные функции
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

; Устанавливает базу дескриптора сегмента
;    в eax передаётся адрес структуры segment_descriptor
;    в ebx передаётся смещение базы относительно cs
init_descriptor_base:
        push   ecx

        ; Получаем базу
        mov     ecx, 0
        mov     cx, cs
        shl     ecx, 4
        add     ecx, ebx

        ; Прописываем её в дескриптор
        mov     (segment_descriptor ptr [eax]).base_low, cx
        shr     ecx, 16
        mov     (segment_descriptor ptr [eax]).base_high0, cl

        pop     ecx
        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

Задания

Разные способы нарушения работы программы:

Просто эксперименты:


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