На границе тучи ходят хмуро, Край суровый тишиной объят. У высоких берегов Амура Часовые Родины стоят Борис Ласкин Время перейти эту реку вброд, Самое время перейти эту реку вброд Борис Гребенщиков
Время пришло! Это замковая глава, она объединяет все предыдущие, сводит в цельную работающую систему и закрывает большую часть «хвостов».
Тема сложная и неоднородная – не существует одной единственной ниточки, потянув за которою можно было бы последовательно и логично описать процесс передачи управления в защищённом режиме. Приходится подходить с разных сторон, потом как-то соединять вместе полученные результаты… Причём эта последовательность выполняется два раза подряд.
Не огорчайтесь, если вам придётся прочитать эту главу пару раз. Представьте себе, сколько времени я её писал и сколько раз за это время перечитывал соответствующие места [Intel 2004] :)
Немного подумав над главой «Теоретическое введение в защиту», а заодно и над всеми предыдущими, можно построить упрощённую модель гипотетической защищённой ОС.
Приблизительная структура ОС изображёна на Рисунке 1 (красным обозначены дескрипторы GDT и IDT, к которым приложение не имеет доступа). Правда, пока неясно, зачем нужен Синий Сегмент Состояния Задачи, но интуиция уже подсказывает, что без него тут не обойтись…
Более-менее то же самое, с небольшими поясняющими комментариями, описывает Таблица 1.
Дескриптор… | DPL | Результат |
---|---|---|
… сегмента, содержащего код ядра ОС. | 0 | Ядро ОС имеет все необходимые ему привилегии. |
… сегмента, содержащего стек ядра ОС. | 0 | К стеку имеет доступ только ядро. |
… сегмента, содержащего данные ядра ОС. | 0 | К данным имеет доступ только ядро. |
… сегмента, содержащего код пользовательского приложения. | 3 | Приложение не имеет никаких лишних возможностей. |
… сегмента, содержащего стек пользовательского приложения. | 3 | В качестве стека такой сегмент может использоваться только пользовательским приложением. Но поскольку любой стек это, прежде всего, сегмент данных, ядро тоже имеет к нему доступ. |
… сегмента, содержащего данные пользовательского приложения. | 3 | Приложение имеет доступ к данным. И ядро, при необходимости, тоже. |
… шлюза вызова, запрещённого для использования из пользовательских приложений (на картинке нет). | 0 | При попытке приложения выполнить инструкцию call с соответствующим селектором, будет сгенерировано исключение #GP. |
… шлюза вызова, разрешённого для использования из пользовательских приложений (на картинке нет). | 3 | Приложения могут использовать соответствующие системные функции. |
… шлюза ловушки/прерывания, запрещённого для «явного» вызова из пользовательских приложений. | 0 | При попытке приложения выполнить инструкцию int n с соответствующим номером, будет сгенерировано исключение #GP. Относится так же к инструкциям int 3 и into. На обработку аппаратных прерываний и исключений этот механизм не влияет, они будут обработаны независимо от уровня DPL. |
… шлюза ловушки/прерывания, разрешённого для «явного» вызова из пользовательских приложений. | 3 | Приложения могут использовать соответствующие прерывания. |
… сегмента состояния задачи. | Любой | Об этом ниже. |
Схема работы такой ОС достаточно очевидна (многозадачности нет):
Во время работы приложения могут генерироваться исключения, происходить внешние прерывания, вызываться программные прерывания. Всё это отражено на Рисунке 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 | Код ядра и код пользовательского режима должны пользоваться различными стеками, при передаче управления должно происходить переключение стека. | Код с различным уровнем привилегий должен использовать разные сегменты стека, при передаче управления с изменением уровня привилегий стек должен переключаться. Как это реализуется – разберёмся чуть позже. |
Оставим пока в покое переключение стека, чуть более подробно рассмотрим все остальные требования и следствия из них.
Это требование очерчивает область, которой защита вообще не касается: передачи управления внутри приложения/ядра – их личное дело, больше они никого волновать не должны. Тем более что при этом не происходит изменения уровня привилегий, то есть ничего значимого с точки зрения защиты.
Таким образом, всегда разрешены:
Требуется возможность вызвать из кода приложения некоторый более привилегированный код для выполнения сервисных функций (системная функция, программное прерывание), обработки особых ситуаций (исключение), взаимодействия с аппаратурой (внешние прерывание).
То есть, возвращаясь к x86, шлюзы ловушек/прерываний/вызовов можно настроить так, что, по крайней мере, некоторые из них будут доступны для пользовательских приложений.
Из главы «Теоретическое введение в защиту» уже известно, что:
Именно через шлюзы налаживается связь между пользовательским кодом и кодом ОС.
После обработки прерывания/исключения необходимо вернуться назад. Для x86 это означает возможность прямой передачи управления от более привилегированного сегмента кода к менее привилегированному инструкциями iretd и retf. Причём передачи в любую точку, не обязательно к месту вызова, достаточно изменить в стеке адрес возврата.
Это, кстати, и есть способ, который был нужен в Требовании (1).
Соображения безопасности запрещают прямую передачу из менее привилегированного сегмента в более привилегированный – только через шлюз. Помимо прочего, запрещена прямая передача управления инструкциями iret, iretd и retf. То есть, если вызвать менее привилегированный код через шлюз (например, если поместить в менее привилегированный сегмент кода обработчик прерывания), обработчик не сможет стандартным способом вернуть управление.
Видимо, именно из этого исходили разработчики, когда запрещали передачу управления через шлюз в менее привилегированный сегмент кода.
Изменение уровня привилегий в сторону уменьшения привилегий – возврат из обработчика исключения/прерывания инструкцией 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) |
Что не попало в таблицу:
Несмотря на несколько «умозрительный» способ построения таблицы, она удивительным образом совпала с правильным вариантом :) Единственное, в ней не упомянуты RPL, которые иногда всё-таки оказываются важны.
На этом завершим слегка затянувшееся введение, и начнём уже двигаться вперёд.
- Каждому шлюзу – 10-и битную защиту! - Ура!!! Митинг продолжается :)
Для начала рассмотрим шлюз вызова. С точки зрения защиты, в процессе участвуют следующие параметры:
Все они обозначены и пронумерованы на Рисунке 3.
Условия успешной передачи управления (все условия должны выполняться одновременно):
То есть, текущий уровень привилегий (с учётом RPL) должен оказаться достаточным для того, чтобы воспользоваться шлюзом, но при этом он (без учёта RPL) не должен превосходить уровень привилегий кода вызываемой функции. Если что-то не так – исключение #GP.
А как же параметр (4) – RPL селектора из дескриптора шлюза? А он просто игнорируется :)
ПРИМЕЧАНИЕ При условии, что дескрипторы создаются системой один раз при инициализации, это довольно разумно. Так как, если этот RPL запрещает вызов, через шлюз передать управление не сможет никто – зачем такой шлюз? Значит, если шлюз есть, передача разрешена, а значит RPL можно не проверять :) Другое дело, если бы дескрипторы генерировались динамически… Но такой подход к проектированию ОС разработчики процессора, видимо, не рассматривали. |
Со шлюзами ловушки/прерывания всё точно так же, только проще, так как параметров меньше.
Смотрите рисунок 4.
Ну и условия успешной передачи управления (все условия должны выполняться одновременно):
То есть, то же самое, что у шлюза вызова, но без RPL.
Как вы помните, любое изменение уровня привилегий должно сопровождаться переключением стека. А поскольку уровень привилегий может измениться только при передаче управления, сценарий выглядит примерно так:
Основной вопрос, конечно, где же то «откуда-то», которое содержит данные о подходящем стеке. Тут есть два варианта.
При передаче управления с повышением уровня привилегий, источником данных о стеке служит сегмент состояния задачи (TSS). В главе «Введение в многозадачность: одна задача» он был описан несколько неподробно, в таблице 4 и на рисунке 5 приведено более детальное описание.
Смещение | Содержимое |
---|---|
0 | … |
4 | 4 байта – значение смещения стека нулевого уровня привилегий. |
8 | 2 байта – значение селектора стека нулевого уровня привилегий. |
12 | 4 байта – значение смещения стека первого уровня привилегий. |
16 | 2 байта – значение селектора стека первого уровня привилегий. |
20 | 4 байта – значение смещения стека второго уровня привилегий. |
24 | 2 байта – значение селектора стека второго уровня привилегий. |
28…104 | … |
Описание соответствующей структуры на ассемблере:
; Сегмент состояния задачи 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 показано состояние старого и нового стеков сразу же после вызова функции с двумя параметрами.
Стек обработчика исключения/прерывания выглядит точно так же, только вместо параметров он будет содержать EFLAGS.
Передача управления с понижением уровня привилегий – фактически возврат из функции или обработчика, данные о старом стеке, так же как и данные об адресе, извлекаются из стека. Подразумевается, что, либо перед этим была выполнена корректная передача «с повышением», либо стек был подготовлен вручную.
Сценарий:
А подготовка стека вручную выглядит, например, так:
; подготовка стека для "возврата" в пользовательский режим 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. |
Наконец, объединим вместе всё, что нам известно про передачу управления, защиту и стеки.
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
Рассмотрим законченный сценарий, пусть это будет программное прерывание.
К сожалению, мне не удалось изобразить всю последовательность на картинке, поместилась только работа со стеками. Передача управления при обработке программного прерывания была более-менее рассмотрена в главе «Программные прерывания» и не слишком сильно изменилась с тех пор.
Сценарии для исключения, внешнего прерывания и вызова функции через call почти ничем не отличаются, поэтому не приводятся полностью. Отличия, о которых всё же стоит упомянуть:
Последний раз алгоритмы переключения режимов упоминались в главе «Программные прерывания», с тех пор мы узнали много нового, пора их обновить. Из реального режима в защищённый:
Обратное переключение:
В общем, если не считать всяких необязательных деталей (страничная адресация и LDT, но они действительно необязательны – если не используются, их можно не инициализировать), это полные, правильные алгоритмы, соответствующие написанному в [Intel 2004]. Не прошло и девяти глав :)
- Какая от него польза, Мастер? - Никакой, насколько я знаю. - ... (и далее по тексту до конца абзаца) Урсула Ле Гуин, «Волшебник Земноморья»
В главе «Теоретическое введение в защиту» был рассмотрен единственный разумный способ использования 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.
Как она это делает:
На что ещё стоит обратить внимание (при написании примера мне было сложнее всего найти ошибки именно в этих деталях):
Надеюсь, после таких пояснений, проблем с пониманием примера не возникнет.
; 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 |
Разные способы нарушения работы программы:
Просто эксперименты: