понедельник, 26 августа 2013 г.

PhantomEx: Переход в пользовательский режим - теория

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

Однако, настоящие ОС никогда не работают в режиме ядра, за исключением разве что однозадачных систем реального режима, где понятия уровня привилегий не существует (например MS DOS). Главная задача любой ОС - обеспечивать среду выполнения пользовательских программ, выполнять их запуск, взаимодействие с ресурсами компьютера и между собой, а так же корректное их завершение. Любая ОС нужна именно для того чтобы выполнять некую полезную работу. Ядро само по себе бессмысленно без пользовательского окружения.

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

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




1. Уровни привилегий. Механизмы защиты в архитектуре x86


Итак, в архитектуре x86, в защищенном режиме процессоров Intel изначально имеется четыре уровня привилегий, или, так называемых колец защиты.


Обычно операционные системы не используют все представленные уровни привилегий, хотя бы даже из-за того, что в режиме страничной адресации памяти доступны только два из них - Ring 0 и Ring 3. Все четыре уровня доступны только при использовании сегментной модели памяти, которая в настоящее время используется в весьма ограниченном варианте. Системой, использовавшей три уровня привилегий - 0, 2 и 3 была коммерческая ОС от IBM OS/2.

Самым привилегированным уровнем является нулевой. Однако, начиная с процессора Intel 386 SL появился так называемый режим гипервизора - режим SMM (System Management Mode), называемый иногда "Кольцо -2" и режим аппаратного гипервизора (VT/AMD-v) ("Кольцо -1"). Однако эти режимы нормальным способом не доступны для программиста, поэтому рассматривать мы их не будем.

Кольца защиты различаются набором команд процессора, которые разрешено исполнять в каждом из режимов. Для пользовательских приложений используется наименее привилегированный уровень 3. А вот какие программы будут считаться пользовательскими - это определяет архитектура системы. В некоторых ОС реального времени на пользовательском уровне могут работать даже драйвера устройств, таким образом не отличаясь от обычных прикладных программ. Однако в большинстве популярных ОС драйвера работают в нулевом кольце защиты, вместе с ядром.

Процессор проверяет привилегии непосредственно перед каждым обращением к памяти, и если происходит нарушение защиты, генерируется исключение #GP. Проверки уровней привилегий происходят всегда, когда процессор работает в защищенном режиме, и механизм защиты нельзя отключить, не выходя из PM. Однако, можно использовать один максимальный уровень привилегий - нулевой, тем самым создав видимость отсутствия защиты. Именно так мы и поступали до сих пор - наше ядро работает на уровне нулевого кольца.

За механизмы защиты отвечают следующие биты и поля:

В дескрипторах сегментов:
  • бит S - системный сегмент;
  • поле типа - тип сегмента, включая запреты на чтение/запись;
  • поле лимита сегмента;
  • поле DPL, определяющее уровень привилегий сегмента или шлюза, указывает, по крайней мере, какой уровень привилегий должна иметь программа, чтобы обратится к этому сегменту или шлюзу;
 В селектора сегментов:
  • поле RPL, определяющее запрашиваемые привилегии, позволяет программам, выполняющимся на более высоком уровне привилегий обращаться к сегментам, как будто их уровень привилегий ниже;
  • поле RPL селектора, загруженного в CS, называется CPL и является текущим уровнем привилегий программы;
В элементах таблиц страниц
  • бит U - определяет уровень привилегий страницы;
  • бит W - разрешает/запрещает запись в страницу.
На уровне сегментов происходят несколько типов проверок. Рассмотрим их по порядку.

Проверка лимитов

Поле лимита в дескрипторе запрещает доступ к памяти за пределами сегмента.  Если бит G дескриптора равен нулю, занчения лимита могут быть от 0 до 0xFFFF (1 Мб). Если же бит G установлен - от 0xFFF (4 Кб) до 0xFFFFFFFF (4 Гб). Для сегментов, растущих вниз (сегмент стека), лимит принимает значения от плюс 1 до 0xFFFF для 16 битных сегментов данных и до 0xFFFFFFFF - для 32-битных. Эти проверки отлавливают такие ошибки, как неправильное вычисление адресов.

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

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

Проверка типа сегмента


Загрузка селектора (и дескриптора) в регистр
  • в CS можно загрузить только сегмент кода;
  • в DS, ES, FS, GS можно загрузить только селектор сегмента данных, сегмента кода доступного для чтения, или нулевой селектор;
  • в SS можно загрузить только сегмент данных, доступных для записи;
  • в LDTR можно загрузить только сегмент LDT;
  • в TR можно загрузить только сегмент TSS/
Обращение к памяти:
  • никакая команда не может писать в сегмент кода;
  • никакая команда не может писать в сегмент данных, защищенный от записи;
  • никакая команда не может читать сегмент кода, защищенный от чтения;
  • нельзя обращаться к памяти, если селектор в сегментном регистре нулевой.
Исполнение команды, использующей селектор в качестве операнда:
  • дяльние CALL и JMP могут выполнятся только в сегмент кода, шлюз вызова, шлюз задачи и сегмент TSS$
  • команда LLDT может обращаться только  к сегменту LDT;
  • команда LTR может обращаться только к сегменту TSS;
  • команда LAR может обращаться только к сегментам кода и данных, шлюзам вызова и задачи, LDT и TSS.
  • команда LSL может обращатся только к сегментам кода, данных, LDT и TSS;
  • элементами IDT могут быть только шлюзы прерываний, ловушек и задач.
Некоторые внутренние операции:
  • при переключении задач целевой дескриптор может быть только TSS или шлюзом задачи;
  • при передаче управления через шлюз сегмент, на который указывает этот шлюз, должен быть сегментом кода (или TSS для шлюза задачи);
  • при возвращении из вложенной задачи селектор в поле связи TSS должен быть селектором сегмента TSS.

Проверка привилегий


Все неравентсва здесь арифметические, то есть A > B означает, что уровень привилегий A меньше, чем B:
  • при загрузке регистра DS, ES, FS или GS должно выполнятся условие DPL ≥ max(RPL, CPL);
  • при загрузке регистров CS и SS должно выполнятся условие DPL = CPL = RPL;
  • при дальних CALL, JMP, RET на неподчиненный сегмент кода должно выполнятся условие DPL = CPL (RPL игнорируется);
  • при дальних CALL, JMP, RET на подчиненный сегмент кода должно выполнятся условие CPL ≥ DPL. При этом CPL не меняется;
  • при дальнем CALL на шлюз вызова должны выполнятся условия: CPL ≤ DPL шлюза, RPL ≤ DPL шлюза, CPL ≥ DPL сегмента;
  • при дальнем JMP на шлюз вызова должны выполнятся условия: CPL ≤ DPL шлюза, RPL ≤ DPL шлюза, CPL ≥ DPL сегмента, если он подчиненный, CPL = DPL сегмента, если он неподчиненный.
При вызове процедуры через шлюз на неподчиненный сегмент кода с другим уровнем привелегий процессор выполняет переключение стека. В сегменте TSS текущей задачи всегда хранятся занчения SS:ESP для стеков уровней привилегий 0, 1 и 2 (но не стек для уровня 3, потому что нельзя выполнять передачу управления на уровень 3, кроме как при помощи команд RET/IRET). 

При переключении стека в новый стек помещаются, до адреса возврата, параметры (их число указано в дескрипторе шлюза вызова), флаги или код ошибки (в случае INT), старые значения SS:ESP, которые команда RET/IRET использует для обратного переключения. То, что надо выполнить возврат из процедуры RET определяет так: RPL селектора, оставленного в стеке, больше (менее привилегированный), чем CPL.

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

Выполнение привилегированных команд

  1. Команды LGDT, LLDT, LTR, LIDT, MOV CRn, LMSW, CLTS, MOV DRn, INVD, WBINVD, INVLPG, HLT, RDMSR, WRMSR, RDPMC, RDTSC, SYSEXIT могут выполнятся только если CPL = 0 (хотя биты PCE и TSD сегмента CR4 разрешают использование команд RDPMC, RDTSC с любого уровня.
  2. Команды LLDT, SLDT, LTR, STR, LSL, LAR, VERR, VERW и ARPL можно выполнять только в защищенном режиме - в реальнои и V86 возникает исключения #UD.
  3. Команды CLI и STI выполняются, только если CPL ≤ IOPL (IOPL - это двухбитная область в регистре флагов, задающая уровень привелегий ввода/вывода). Если установлен бит PVI в регистре CR4 эти команды выполняются с любым CPL, но управляют флагом VIF, а не IF
  4. Команды IN, OUT, INSB, INSW, INSD, OUTSB, OUTSW, OUTSD выполняются только если CPL ≤ IOPL и если бит в битовой карте ввода-вывода, соответствующий данному порту равен нулю. (Эта карта - битовое поле в сегменте TSS каждый бит которого отвечает за оди порт ввода-вывода. Признаком её конца служит слово, в котором все 16 бит установлены в 1).

Защита на уровне страниц

  1. Обращение к странице памяти с битом U в атрибуте страницы или таблицы страниц, равным нулю, приводит к исключению #PF, если CPL = 3.
  2. Попытка записи в страницу с битом W в атрибуте страницы или атрибуте таблицы страниц равным нулю, с CPL = 3 приводит к исключению #PF
  3. Попытка записи в страницу с битом W в атрибуте страницы или атрибуте таблицы страниц равным нулю, если бит WP в регистре CR0 равен 1, приводит к исключению #PF.

2. Программное  переключение задач с разным уровнем привилегий


Архитектура x86 имеет ту особенность, что в ней не существует явного задания CPL. Согласно документации Intel текущий уровень привилегий определяется "теневой" частью регистра CS и задается при загрузке этого регистра.

Однако, регистр CS нельзя загрузить явным способом. При формировании таблицы GDT мы делали это неявно, путем осуществления дальнего перехода, использовав в качестве селектора 0x08, то есть селектор сегмента кода ядра

    .
    .
    .
    ljmp    $0x08,$flush
   
flush:
    ret

Однако передать управление на уровень привилегий 3 нельзя с помощью явного перехода командами JMP и CALL, о чем писалось выше.

В этом случае на помощь приходит команда возврата из прерывания IRET (IRETD в 32-разрядном варианте). При выполнении этой команды из стека извлекаются и помещаются в соответствующие регистры значения EIP, CS и EFLAGS. Если извлеченный из стека селектор сегмента кода будет указывать на менее привилегированный сегмент, то  - внимание!!! - из стека будут взяты ещё два значения и загружены в ESP и SS соответственно! Дополнив эти действия "ручной" загрузкой регистра DS селектором сегмента данных с меньшим DPL мы таким образом переключим процессор в менее привилегированный режим. Таким образом, перед переходом на уровень привилегий ring 3 необходимо сформировать в текущем стеке такой кадр


 Таким образом будет произведен переход в пользовательский режим, сразу после выполнения команды IRETD.

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

Используемый нами алгоритм переключения через стек задачи будет прекрасно работать на любом уровне привилегий. Однако необходимо обеспечить ещё одно требование - сформировать один сегмент TSS, в котором будут хранится значения SS0 и ESP0 указывающие на расположение и состояние стека ядра. Значение ESP0 необходимо модифицировать при каждом переключении.

Что представляет собой TSS? В документации Intel для процессора 80386 имеется очень хорошее описание этого сегмента, который называется сегментом состояния задачи.

Таблица 19. Сегмент состояния задачи.

СмещениеПолеНазначение
+00рPrev_TSSСелектор предыдущей задачи (страшее слово содержит нули)
+04hESP0Указатель стека для CPL = 0
+08hSS0Селектор стека для CPL = 0
+0СhESP1Указатель стека для CPL =1
+10hSS1Селектор стека для CPL = 1
+14hESP2Указатель стека для CPL =2
+18hSS2Селектор стека для CPL = 2
+1СhCR3Адрес каталога таблиц страниц задачи
+20hEIPАдрес возврата при переключении
+24hEFLAGSФлаги задачи
+28hEAXСостояние регистра EAX задачи
+2СhECXСостояние регистра EСX задачи
+30hEDXСостояние регистра EDX задачи
+34hEBXСостояние регистра EBX задачи
+38hESPСостояние регистра ESP задачи
+3СhEBPСостояние регистра EBP задачи
+40hESIСостяние регистра ESI задачи
+44hEDIСостяние регистра EDI задачи
+48hESСостяние регистра ES задачи
+4СhCSСостяние регистра CS задачи
+50hSSСостяние регистра SS задачи
+54hDSСостяние регистра DS задачи
+58hFSСостяние регистра FS задачи
+5СhGSСостяние регистра GS задачи
+60hLDTRЛокальная таблица дескрипторов задачи
+64hTask_flagsслово флагов задачи, занчимым является только бит 0 - флаг T: вызывает #DB при переключении на задачу.
+66hIOMAP_ADDR16-битное смещение от начала TSS по которому начинается битовая карта ввода вывода и заканчивается карта перенаправления прерываний данной задачи

При использовании аппаратного переключения задач, такой сегмент необходимо создавать для каждой задачи. Причем дескриптор сегмента TSS находится в GDT, попытка загрузить его в LDT приведет к исключению #GP. Учитывая, что "емкость" GDT составляет 8191 дескриптор, из которых при использовании плоской модели памяти и разных уровней привелегий 5 обязательно заняты (нулевой дескриптор, дескрипторы кода и данных ядра, дескрипторы кода и данных пользовательского режима), то с такой многозадачностью особенно и не разгуляешся, если речь идет о мощной вычислительной системе.

При программном переключении задач мы будем иметь один TSS, дескриптор которого придется сформировать и загрузить в GDT, а селектор загрузить в регистр TR.

Формат дескриптора TSS несколько отличается от формата дескриптора сегмента кода/данных

Таблица 6. Формат сегментного дескриптора

БайтНазначение
0-115-0 биты лимита сегмента
2-315-0 биты базы сегмента
423-16 биты базы сегмента
5Байт доступа:

бит 0: 1
бит 1: B - бит занятости, устанавливается при обращении;
бит 2: 0
бит 3: 1
бит 4: 0
бит 5-6: DPL - уровень привилегий сегмента
бит 7: P - бит присутствия сегмента
6бит 3-0: 19-16 биты лимита;
бит 4: зарезервировано для операционной системы;
бит 5: 0
бит 6: 0
бит 7: бит гранулярности (0 - лимит в байтах; 1 - лимит в 4-килобайтных единицах)

7биты 31-24 базы сегмента.

Сформированный в таком формате дескриптор нужно загрузить в GDT, а затем загрузить селектор TSS в регистр TR.

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

Заключение


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