Уровни привилегий и правила В реальном режиме процессор имеет все привилегии. В защищенном и виртуальном они могут быть ограничены. Это уменьшает возможность разрушительного воздействия вредных команд. Основные правила: 1) запрещается доступ к более привилегированным данным или их вызов кодом, который менее привилегированный. Дополнительные правила: 1) вызов более привилегированных данных не может быть осуществлен из произвольного адреса, необходимо использовать адресацию определенную системой; 2) при вызове более привилегированного кода необходимо удостовериться, что есть достаточно пространства для стека. Существует 4 уровня: Уровень 0 имеет все привилегии (кроме регистров отладки, которые закрыты для доступа даже для первого уровня; некоторые инструкции зарезервированы только для уровня 0) Остальные уровни менее привилегированные. Следующие термины используются для режимов: CPL - Current PL, DPL - Descriptor PL, PRL - Requested PL (в селекторе), IOPL (для флагов) - макс.CPL разрешающий I/O команды. (cli, sti, pushf, popf, ...) При доступе к нужному кодовому сегменту, правила привилегий требуют max(CPL,RPL)<=DPL. Для запуска кода (командой FAR CALL или JMP) необходимо DPL<=CPL. Для передачи управления коду с меньшим PL (более привилегированному), необходимо осуществлять вызов через шлюз (gate). В этом случае необходимо: max(CPL,RPL)<=gate_DPL. Но если шлюз обращается к коду, то необходимо, чтобы code_DPL<gate_DPL; Шлюз - это значение в таблице GDT или LDT. Правила привилегий требуют, чтобы target_code_DPL <= CPL для CALL или JMP. Также необходимо, чтобы TR указывал на существующий TSS, потому что это меняет стек: старый SS:[E]SP помещается в новый стек, а затем помещаются параметры вызова и, наконец, помещается CS:[E]IP. При возврате из вызова, CPU определяет PRL для CS в стеке > CPL и переключает стек обратно (если =, то не переключает, < запрещено правилами привилегий.) Для правильного функционирования параметры для возврата и вызова должны совпадать. Для сегмента стека DPL должно совпадать с CPL (поэтому в более привилегированном режиме не возможен сбой из-за неверных настроек стека в менее привилегированном режиме, и в менее привилегированном режиме нет возможности для доступа к более привилегированному стеку). RPL используется системой для блокирования возможности передачи указателя от пользовательского кода, который неверный для пользовательского режима, но верный для системного: система использует RPL как пользовательский код и вызывает в этом случае ошибку нарушения. Это может быть сделано используя команду ARPL, которая настраивает RPL на селектор, а затем устанавливает флаг нуля (информируя ОС об ошибке доступа). ОС использует это для установки указателя RPL на код программы. Для того, чтобы проверить, какой доступ можно получить, можно использовать команды VERR, VERW, LAR, LSL. Все они устанавливают флаг нуля, если получили доступ, если нет, то очищают. Первые две просто проверяют доступ для чтения/записи, LAR определяет биты устанавливающие права доступа для сегмента, LSL определяет ограничение сегмента. Эти команды могут проверить, что вызовет ошибку доступа, до тех пор, пока эта ошибка случится. Некоторые инструкции разрешены только если CPL = 0. Вот их перечень: CLTS - очистить флаг смена задачи, HLT - остановить процессор, GDTR, IDTR, LDTR, MSW, TR - загрузка системных регистров CRx, DRx, TRx (любой доступ к этим регистрам) Некоторые другие требуют, чтобы CPL <= IOPL. Например: IN, INS, OUT, OUTS, CLI, STI. Также окружение POPF зависит от CPL: если CPL > 0, то IOPL и VM не изменяются инструкцией POPF. Если CPL<IOPL, IF (interrupt enable) не изменяется. Прерывания В каждом режиме существует массив, который содержит информацию о том, какое действие должно быть выполнено в случае прерывания. Первое значение этого массива обращается к INT 0, следующее к INT 1, и так далее. Этот массив называется IDT (Interrupt Descriptor Table). В реальном режиме каждое значение в IDT - обычный FAR адрес процедуры, которая обрабатывает прерывание. Изначально IDT располагается по адресу 0 и содержит 100h записей (400h байтов). На до-286 процессорах адрес и размер таблицы IDT не изменялись, но на 286+ можно загружать и сохранять таблицу используя инструкции LIDT и SIDT. В защищенном режиме IDT содержит 8 байтовые значения, которые могут быть прерываниями, ловушками или шлюзами. Ловушка в отличие от прерывания не изменяет флаг прерывания. Шлюз осуществляет вызов другой задачи. Все они имеют DPL и инструкции прерывания может вызвать ошибку общей защиты если CPL < DPL. Тем не менее, другие прерывания имеют CPL 0, поэтому они могут вызывать любой шлюз. В некоторых ситуациях возникают исключения. Они бывают (для 80386): ошибка деления (0), исключение отладки (1), немаскируемое прерывание (2), точка-останова (3), переполнение (4), проверка границы (5), неверная инструкция (6), не найден сопроцессор (7), ошибка удвоения (8), ошибка сегмента сопроцессора (9), неверный TSS (10), сегмент не существует (11), ошибка стека (12) общая ошибка защиты (13), ошибка страницы (14), ошибка сопроцессора (16). Большинство из них помещает в стек IP инструкции вызвавшей их, кроме 3,4,9, которые помещают IP следующей инструкции. Прерывания не обрабатываются, когда PL>CPL (кроме смены задачи), попытка сделать это вызовет общую ошибку защиты. Обработка прерываний в защищенном режиме более сложная, когда уровень привилегий прерывания отличается от уровня кода. Это осуществляется приблизительно так же как и вызов через шлюз: стек изменяется, новый SS:SP определяется из TSS, старый SS:SP помещается в новый стек, затем помещаются флаги, CS, IP и код ошибки (для некоторых исключений). В виртуальном режиме прерывание помещает в стек GS, FS, DS, ES, SS, ESP, EFLAGS, CS, EIP в стек с PL 0. Затем бит VM включается, чтобы показать, что прерывание обработано в виртуальном режиме. Таблицы дескрипторов (для защищенного режима) Таблица глобальных дескрипторов (GDT) содержит дескрипторы разных типов кроме прерываний и ловушек. Это необходимо для защищенного режима. Первая запись в GDT не используется, она обращается к нулевому селектору, который может быть загружен в сегментный регистр, но вызовет ошибку при адресации памяти. Таблица локальных дескрипторов (LDT) может содержать обычные сегментные регистры (кроме TSS), а также вызовы и ловушки. Обычно каждая задача имеет свою LDT (которая меняется при переключении задачи). LDT должна иметь дескриптор в GDT. Таблица дескрипторов прерываний была уже оговорена в главе Прерывания. Обычные сегментные дескрипторы используются, когда сегментный регистр загружен и они описывают зону памяти, а также предоставляют доступ к ней. Бит 2 используемого селектора выбирает таблицу: 0 означает GDT, а 1 - LDT. Другие дескрипторы могут быть TSS или шлюзами. Они могут быть использованы как кодовые сегменты, например, при помощи FAR JMP или CALL. Это такой вид непрямого перехода или вызова. TSS или шлюз, указывая на TSS, вызывают смену задачи. Шлюзы используются для передачи управления к более привилегированному коду, который не адресуется напрямую. TSS может быть определен используя инструкцию LTR (Load Task Register), это происходит однажды при инициализации защищенного режима. LDT дескриптор может быть загружен в регистр LDTR используя команду LLDT. Обычно это делают один раз. Сегментные и системные дескрипторы Типы сегментов, которые поддерживаются. Для всех типов: бит 7 означает присутствие в памяти, биты 5-6 содержат DPL, который сообщает максимальный CPL, который может обращаться к сегменту 10h+flags - данные : бит 1 - записываемый, бит 2 - расширяемый 18h+flags - код : бит 1 - читаемый, бит 2 - постоянный. бит 0 устанавливается в 1 при всяком доступе. Дескриптор также содержит ограничение в word [0] и базу в byte [2..4]. Байт [6] содержит некоторые дополнительные флаги: бит 7 - зернистость (ограничение в 4КБ страницы), бит 6 - 32-битная адресация (разрешает использовать ESP/EIP), бит 5 - должен быть 0, бит 4 - используется программистом. 01h+flags - TSS: бит 1 - занят, бит 3 - 386 TSS 02h - LDT 04h+flags - шлюз вызова 05h - шлюз задачи 06h+flags - шлюз прерываний: бит 0 - ловушка, бит 3 - 386. Для всех шлюзов word[2] содержит селектор, word[0] и word[3] содержат смещение вызывавшего кода (игнорированного для шлюза задачи), байт[4] содержит число word (0-31) для копирования в случае одноуровневого вызова. TSS и LDT имеют базу и ограничение в той же форме, что и сегменты данных или кода, они могут иметь установленный бит 7 в байте[6] для определения ограничения в страницах. Word[6] должно быть 0 для дескриптора. LDT похожа на GDT, но поддерживаются не все типы дескрипторов. TSS хранит состояния задачи (все регистры: общие, сегментные, флаги, IP, LDTR); он также хранит ссылку к коду, вызвавшему TSS и стек для PL 0,1,2. 386 TSS также хранит бит ловушки отладки (если установлен, то вызывается INT 1 при смене задачи), битовую карту ввода/вывода, а также значение CR3 для задачи. Таблицы страниц памяти Все директории страниц и таблицы страниц хранят значения адреса в битах 31-12, а биты 11-9 используются программистом. Биты 8,7,4,3 должны быть 0. Бит 5 назван А (адресуемый), он устанавливается процессором при доступе. Бит 6 назван D (dirty - грязный), он устанавливается если осуществлена запись в память. Бит 0 назван P (существующий). Все другие игнорируются. Но если бит 2 установлен, то CPL3 получает доступ. Бит 1 разрешает CPL3 записывать. Для CPL < 3 чтение/запись разрешены при любых значения битов. Таблицы страниц памяти обычно кэшированы в процессоре, изменение их в памяти не приведет к изменениям до тех пор, пока не обновится кэш. Кэш обновляется каждый раз, когда CR3 (определяющий первую страницу) перезагружается. Биты 0-11 регистра CR3 должны быть 0. Адресация через страницы осуществляется через таблицы страниц памяти: CR3 + (linear_address SHR 20) AND 0FFCh - адрес в директории страниц. Адрес таблицы страниц + (линейный адрес SHR 10) AND 0FFCh - адрес в таблице страниц и значение в адресе содержит базовый адрес страницы. Комбинируя его вместе с битами 11-0 линейного адреса получим физический адрес. Если страничное отображение памяти включено, CR3 должен хранить физический адрес директории страницы, а все другие адреса должны быть линейными. Переключение в защищенный режим и обратно в реальный Во-первых, необходимо получить управление над возможными ошибками, для этого необходимо сохранить в dword [0467h] адрес, куда будет передано управление, а затем записать 0Ah в регистр CMOS 0Fh. Вот таким образом: cli mov al,8Fh out 70h, al ;(1нс задержка) mov al,0Ah out 71h,al Обычно адресная линия А20 закрыта, её необходимо открыть. Если вы используете HIMEM, то она может быть открыта запросом к HIMEM. Но если вы используете DOS=HIGH, то она обычно открыта, так как она открывается вызовом DOS. В обратном случае необходимо записать значение в порт контроллера клавиатуры перед тем, как включать защищенный режим. Переключение в PM Необходимо загрузить GDTR, затем включить защиту в настройках CR0/MSW: Для 386: mov eax,cr0 or al,1 mov cr0,ax Для 286: smsw ax or al,1 lmsw ax Рекомендуется загружать регистр IDTR непосредственно до или после переключения в режим (один и тот же IDT не может быть верным для обоих режимов). Затем после перехода нужно осуществить JMP для освобождения очереди. Необходимо перезагрузить новыми значениями CS и SS, чтобы не произошло сбоя. Также рекомендуется загрузить все сегментные регистры и LDTR. Перед первой сменой задачи необходимо загрузить TR (селектором ли верным TSS дескриптором). Также существует вызов BIOS для перехода в PM и изменения расположения векторов прерываний, а также включения адресной линии А20. Для информации смотрите INT 15 AH=89h. Возврат в реальный режим Это может быть осуществлено очисткой бита 0 в регистре CR0, но необходимо сделать кое-какую подготовку: 1) необходимо отключить страничное отображение памяти (перейти в код/стек с линейным адресом равным физическому, очистить PG бит в CR0, очистить CR3). 2) перейти в кодовый сегмент с ограничением 64Кб и загрузить все сегментные регистры, кроме кодового. 3) после очистки бита 0 в CR0, необходимо осуществить дальний переход на загрузку CS и очистить очередь. 4) загрузить сегментные регистры для RM. Но всё это не пройдет с процессором 286 у которого нет CR0 (использование LMSW не поможет). Единственный способ вернуться в RealMode - перезапуск процессора. Это осуществляется так: cli xor cx,cx wait_kb: in al,64h test al,2 loopnz wait_kb mov al, 0FEh out 64h,al hlt Заключение Есть программы, работающие в VM86, предоставляющие интерфейс для перехода в PM и обратно в VM86. Они называются VCPI. Необходимо резервировать 3 записи в GDT для VCPI провайдера. Наличие такой программы можно проверить вызовом: INT 67h AH=0DEh. P.S. Огромная благодарность Jerzy Tarasiuk
Уважаемый посетитель!
Чтобы распечатать файл, скачайте его (в формате Word).
Ссылка на скачивание - внизу страницы.