к содержанию |
Мы уже говорили вам, что операционная система MS-DOS, использующая реальный режим работы процессора, не защищена от прикладных программ. Подсистема управления памятью MS-DOS основана на разбиении всей памяти на участки, в начале которых помещаются блоки управления памятью MCB (Memory Control Block). При запуске программы MS-DOS выделяет ей необходимое количество блоков и загружает соответствующим образом сегментные регистры.
Если программа не изменяет содержимое сегментных регистров, она будет работать только с теми сегментами памяти, которые выделены ей операционной системой. Однако ничто не может помешать любой программе загрузить в сегментные регистры любое значение, в том числе адреса сегментов самой операционной системы.
Поэтому, строго говоря, MS-DOS не имеет сколько-нибудь надёжного механизма управления памятью - всё основано на выполнении программами "джентльменского соглашения" о неразрушении операционной системы.
Для повышения надёжности работы всей системы в целом необходимо запретить обычным пользовательским программам модифицировать области памяти, принадлежащие операционной системе. Если система работает одновременно с несколькими пользователями (мультипользовательская операционная система), необходимо также закрыть программам пользователя доступ к памяти, распределённой другим пользователям. Иначе говоря, необходимо разделить адресные пространства операционной системы от пользовательских программ с одной стороны, и адресные пространства отдельных пользователей друг от друга с другой стороны.
Такие операционные системы, как WINDOWS и OS/2, являются однопользовательскими, но многозадачными системами. Пользователь может запустить одновременно несколько программ. В этом случае было бы желательно разделить адресные пространства этих программ с целью исключения их влияния друг на друга (и, разумеется, на операционную систему). Аварийное завершение одной запущенной задачи не должно вызывать аварийного завершения остальных задач или, тем более, всей операционной системы.
Кроме того, необходимо разрешить модулям операционной системы доступ к памяти всех запущенных программ для выполнения эффективного управления работой всей системы.
Отсюда с одной стороны, вытекает идея наделения программ операционной системы некоторыми привилегиями по сравнению с пользовательскими программами, с другой стороны - идея наделения отдельных участков памяти средствами защиты от доступа к ним со стороны непривилегированных программ.
Многие мини-ЭВМ и большие ЭВМ реализуют схему "супервизор-пользователь". Операционная система работает в режиме "супервизор" и ей доступны все ресурсы компьютера - вся память, любые команды процессора и т.д. Запущенные программы работают в режиме "пользователь" и имеют доступ только к тем участкам памяти, которые выделены им операционной системой. Кроме того, эти программы ограничены в использовании некоторых команд процессора (например, команд ввода/вывода).
Для предотвращения доступа программам, работающим в режиме "пользователь" к чужим блокам памяти используются так называемые "ключи" (это просто целые числа). Отдельным блокам памяти операционная система назначает свои "ключи". Кроме того, запущенные программы также снабжаются "ключами". Программа может иметь доступ только к таким блокам памяти, к которым подходит "ключ", имеющийся в распоряжении программы.
Такая схема не обеспечивает полного изолирования адресных пространств отдельных задач. Адресное пространство здесь одно, но для обычных программ разрешён доступ только к своему участку памяти в соответствии с имеющимся ключом. Однако в системе может работать несколько программ с одинаковым ключом памяти!
Процессор i80286 использует более гибкую и надёжную схему защиты операционной системы и программ друг от друга.
В этой схеме используются привилегии четырёх уровней - от 0 до 3. Самые большие привилегии соответствуют уровню 0. Обычно такими привилегиями обладает ядро операционной системы. Минимальные привилегии у пользовательских программ - уровень 3.
Уровни привилегий часто называют кольцами защиты (см. рис. 10).
Рис. 10. Кольца защиты.
Как распределить привилегии программ в операционной системе? Можно использовать, например, такое распределение:
Несложные системы могут использовать не все кольца, а только некоторые или даже одно. Например, можно расположить все программы операционной системы в кольце 0, а пользовательские программы - в кольце 3. Это вариант описанной выше схемы "супервизор-пользователь".
Простейшие системы можно полностью реализовать в нулевом кольце. Именно так мы и сделали в примерах программ, приведённых в этой книге.
Как практически используются кольца защиты?
Вспомним, что любой селектор имеет поле RPL (биты 0 и 1). В регистре CS хранится селектор текущего выполняемого сегмента кода. Этому селектору соответствует дескриптор в таблице GDT или LDT. В дескрипторе, в байте доступа располагается поле DPL (биты 5 и 6). Указанные поля участвуют в механизме защиты памяти.
Когда операционная система подготавливает программу для запуска, она формирует в GDT или LDT дескриптор, описывающий сегмент кода программы. В этом дескрипторе в поле DPL байта доступа проставляется номер кольца, в котором будет работать данная программа - текущий уровень привилегий CPL (Current Privilege Level). То есть возможности программы определяются содержимым поля DPL в байте доступа.
Текущий уровень привилегий копируется в поле RPL селектора сегмента кода, загруженного в регистре CS. Программа всегда может проанализировать свой текущий уровень привилегий исходя из значения поле RPL в регистре CS. Однако она не может изменить свой уровень привилегий простой заменой содержимого поля RPL в сегменте CS.
Итак, программа получает от операционной системы текущий уровень привилегий CPL, который она может проанализировать на основании содержимого регистра CS.
Дескрипторы, описывающие сегменты данных, содержат поле уровня привилегий дескриптора DPL (Descriptor Privilege Level). Поле DPL содержит минимальные привилегии, которые нужны для доступа к сегменту данных.
Перед тем, как обратиться к сегменту данных, программа должна загрузить в один из сегментных регистров селектор, соответствующий нужному сегменту данных. В селекторе необходимо указать поле уровня запрашиваемых привилегий RPL (Requested Privilege Level).
Программе будет предоставлен доступ к сегменту только в том случае, когда уровень привилегий дескриптора запрашиваемого сегмента DPL больше или равен значению max(CPL,RPL), т.е. наибольшему из значений текущего уровня привилегий CPL и уровня запрашиваемых привилегий RPL:
DPL >= max(CPL,RPL)
Если программа попытается получить доступ к более привилегированному, чем она сама сегменту памяти, её выполнение будет прервано.
В наших примерах, реализованных для простоты в кольце 0, все уровни привилегий равны 0, т.е. DPL=CPL=RPL=0.
Поле TYPE дескриптора определяет способ, которым можно использовать тот или иной сегмент. Разделение сегментов на типы позволяет защититься от случайного или преднамеренного использования сегментов не по назначению.
Сегмент кода может быть закрыт для чтения установкой бита R в байте доступа. Такие сегменты можно только выполнять, но нельзя читать. В сегмент кода нельзя записывать какие-либо данные. В регистр CS можно загружать только такие селекторы, которые относятся к сегментам кода.
Сегменты данных могут быть закрыты для записи установкой бита W в байте доступа. Сегменту данных нельзя передать управление, загрузив его селектор в регистр CS.
Дескрипторы некоторых типов, например, описывающих расположение таблицы LDT или сегмента состояния задачи, о котором мы будем говорить позже, нельзя использовать для чтения или записи, даже если программа выполняется в нулевом приоритетном кольце. В случае такой необходимости следует создать дополнительные (алиасные) дескрипторы, в которых эти же сегменты описаны как сегменты данных.
Программы реального режима работают всегда с сегментами размером 64 килобайта. Если программа состоит из нескольких сегментов, то некоторые сегменты могут перекрываться. Существует потенциальная опасность, что в результате программной ошибки (например, выхода индекса массива за допустимые пределы) произойдёт запись в другой сегмент.
Процессор i80286 позволяет создавать сегменты любого размера в пределах 64 килобайт (процессоры i80386 и i80486 могут работать с сегментами размером 4 гигабайта). Кроме того, он следит за тем, чтобы при адресации памяти не происходил выход за границы сегмента.
Границы сегмента задаются полем предела в дескрипторе сегмента. Мы уже говорили, что значение этого поля должно быть равно размеру сегмента в байтах минус единица.
Интерпретация поля предела зависит от состояния бита D поля доступа. Для сегментов стеков необходимо устанавливать поле D равным 1. В этом случае попытка записи в переполненный стек вызовет прерывание программы. В ответ на это прерывание операционная система может, например, выделить дополнительную память для стека.
В реальном режиме переполнение стека никак не контролируется и может привести к разрушению самой программы или операционной системы.
Для обеспечения надёжности работы операционной системы необходимо ограничить использование обычными программами некоторых команд процессора. Например, обычные программы не должны иметь доступа к командам загрузки системных регистров GDTR и LDTR.
В таблице 2 приведён список привелигерованных команд, которые могут выполняться только в нулевом приоритетном кольце, т.е. только теми программами, которые имеют наибольшие привилегии (CPL=0).
Таблица 2. Привилегированные команды процессора i80286.
Команда | Выполняемые функции |
LGDT | Загрузка регистра глобальной таблицы дескрипторов GDTR |
LLDT | Загрузка регистра локальной таблицы дескрипторов LDTR |
LIDT | Загрузка регистра таблицы дескрипторов прерываний IDTR |
LTR | Загрузка регистра задачи TR |
LMSW | Загрузка слова состояния машины MSW |
CLTS | Сброс флага переключения задачи |
HLT | Останов процессора |
В этой таблице вам знакомы только команды LGDT и LLDT, остальные команды мы разберём позже, по мере изучения различных возможностей процессора i80286, таких как обработка прерываний в защищённом режиме и мультизадачность.
Существуют команды, которые должны выполняться не только в нулевом кольце, но использование которых должно быть запрещено для программ, имеющих уровень привилегий ниже некоторого, заданного для всей системы в целом. Кроме того, функции некоторых команд было бы желательно модифицировать в соответствии с привилегиями выполняющей их программы. Такие команды называются чувствительными.
Кроме привилегированных команд, необходимо ограничить обычные программы в использовании команд ввода/вывода: IN, OUT, INS, OUTS. С помощью этих команд программа может, например, перепрограммировать контроллеры прерываний и прямого доступа к памяти, что в свою очередь откроет путь к непосредственной модификации памяти по физическим адресам.
Также не следует разрешать обычным программам использовать команды, которые могут заблокировать прерывания: CLI, STI, LOCK. В мультизадачной среде обычные программы не должны иметь возможность отключать механизм разделения времени между задачами.
Регистр FLAGS флагов процессора i80286 в разрядах 12-13 содержит двухбитовое поле IOPL (Input/Output Privilege Level). Это поле определяет наименее привилегированное кольцо, в котором разрешено использовать команды ввода/вывода.
Программы, работающие не в нулевом кольце, не могут сами модифицировать поле IOPL регистра FLAGS. При выполнении команд, загружающих регистр флажков (IRET и POPF) не в нулевом кольце, поле IOPL не модифицируется. Аналогично, без изменения остаётся флаг разрешения прерываний IF.
В реальном режиме передача управления выполняется с помощью команд JMP, CALL, INT, RET, IRET, а также при возникновении прерываний. Внутрисегментная передача управления выполняется командами JMP, CALL, RET, а межсегментная передача управления - командами JMP, CALL, INT, RET, IRET и в случае возникновения прерываний.
В защищённом режиме всё происходит аналогично. При внутрисегментной передаче управления в регистр IP заносится новое значение, а регистр CS не модифицируется. Межсегментная передача подразумевает одновременное изменение регистров CS и IP, а также в некоторых случаях и регистра флагов FLAGS (прерывания и команды RET, IRET).
Операционная система MS-DOS, работающая в реальном режиме, позволяет любой программе вызывать любые подпрограммы. Единственное условие для успешного вызова подпрограммы - знание адреса подпрограммы (сегмента и смещения) и формата передаваемых регистров.
При проектировании защищённых систем необходимо предотвратить возможность непосредственного вызова произвольных привилегированных подпрограмм менее привилегированными. С другой стороны, необходимо обеспечить существование механизма вызова непривилегированной программой модулей операционной системы (привилегированных) для получения обслуживания. Например, для того чтобы узнать текущее время или выполнить обращение к магнитному диску непривилегированная программа должна вызвать соответствующие модули операционной системы.
Процессор i80286 содержит все средства, необходимые для организации с одной стороны, защиты модулей операционной системы от несанкционированного вызова прикладными программами и, с другой стороны, для обслуживания модулями операционной системы вызовов прикладных программ.
При использовании команд CALL или JMP для внутрисегментной передачи управления в качестве операнда для команд необходимо указывать смещение модуля в текущем сегменте, которому будет передано управление. При передаче управления процессор проверяет смещение на предмет выхода за пределы текущего сегмента кода. В случае использования этих команд для межсегментной передачи управления существуют две возможности.
Во-первых, программист может указать в качестве операнда селектор дескриптора вызываемого сегмента кода.
Во-вторых, программист может указать в качестве операнда селектор, которому соответствует дескриптор специального типа - вентиль вызова. Вентиль вызова содержит логический (а не физический, как дескриптор сегмента) адрес вызываемого модуля.
Первый способ называется прямым вызовом, второй - вызовом через вентиль вызова.
Рассмотрим сначала прямой вызов сегмента.
В этом случае операнд команды вызова - селектор дескриптора вызываемого или, другими словами, целевого сегмента.
На выполнение прямого вызова целевого сегмента или прямого перехода к целевому сегменту влияет бит подчинения C, который располагается в бите 2 байта доступа дескриптора целевого сегмента.
Если бит C установлен в 0, целевой сегмент называется несогласованным. Несогласованный сегмент может быть вызван только такой программой, которая имеет большие или такие же привилегии, что и целевой сегмент. Т.е. должно выполняться условие CPL <= DPL.
Обычная программа, выполняющаяся в кольце 3, не может вызывать несогласованный сегмент (или передавать управление несогласованному сегменту), находящемуся в кольцах 0, 1 или 2. Этот механизм блокирует несанкционированный вызов модулей операционной системы программами пользователя.
Однако должен существовать способ безопасного вызова модулей операционной системы в заранее оговорённых точках для того, чтобы программы могли получать обслуживание от привилегированных модулей ядра операционной системы. Прямой вызов несогласованного сегмента здесь не подходит, так как ядро располагается в нулевом кольце.
Если программный сегмент, располагающийся в ядре операционной системы, должен вызываться как самой операционной системой в нулевом кольце так и программами пользователя в менее привилегированных кольцах, можно применить согласованный сегмент.
В согласованном сегменте кода бит подчинения C байта доступа установлен в 1. Согласованный сегмент можно вызывать из программ, находящихся в любом кольце. Но в любом случае при вызове этот сегмент будет выполняться с привилегиями вызывающей программы. Если согласованный сегмент вызывается из кольца 0, он будет выполняться с уровнем привилегий 0, если из кольца 3 - с уровнем привилегий 3.
Другой способ выполнить межсегментную передачу управления - использовать передачу управления через вентиль вызова.
В этом случае команды CALL или JMP адресуются к дескриптору с типом вентиля вызова. Формат этого дескриптора показан на рис. 11.
Рис. 11. Дескриптор вентиля вызова.
Селектор и смещение представляют собой адрес вызова или передачи управления в целевом сегменте. Поле счётчика слов используется при передаче параметров вызываемому модулю.
Поле DPL вентиля вызова указывает минимальный уровень привилегий, необходимый для получения доступа через данный вентиль. Операционная система может подготовит для программ пользователя вентили, доступные из кольца 3 и обеспечивающие вызов процедур обслуживания, располагающихся в ядре операционной системы в кольце 0. Однако программы 3 кольца смогут обращаться только к таким вентилям вызова, которые содержат в поле DPL значение 3. При этом управление через вентиль вызова передаётся только в ту точку, которая описана в данном вентиле и является безопасной с точки зрения операционной системы. Программа пользователя не сможет передать управление в середину модуля или в середину машинной команды.
Но есть ещё проблема с передачей параметров. Обычно передача параметров выполняется через стек. Но для обеспечения защиты программы нулевого кольца используют отдельный стек (вообще говоря, для каждого кольца используется отдельный стек, механизм переключения стеков мы рассмотрим при описании мультизадачности).
При вызове сегмента через вентиль вызова процессор копирует из стека вызывающей программы в стек целевого сегмента такое количество 16-битовых слов, которое указано в поле 5-разрядного счётчика слов вентиля вызова. Таким способом вызываемому модулю можно передать до 31 параметра.
Вентиль вызова - идеальное средство для предоставления обычным программам сервиса со стороны операционной системы. С одной стороны вентиль вызова, формируемый операционной системой, позволяет непривилегированным программам передавать управление привилегированным кодовым сегментам для получения необходимого обслуживания. С другой стороны, операционная система имеет возможность держать под своим контролем использование вентилей вызова, задавая в поле DPL вентиля минимальный уровень привилегий, которыми должна обладать программа, передающая управление через этот вентиль, а также безопасный адрес входа в целевом сегменте.
Команды RET и IRET предназначены для возврата из подпрограмм и процедур обработки прерываний, соответственно.
Команда RET может возвращать управление в пределах одного сегмента (внутрисегментная форма) или в другой сегмент (межсегментная форма).
При выполнении внутрисегментной формы команды RET процессор выполняет проверку превышения границы текущего сегмента, не позволяя передавать управление за его пределы.
Когда выполняется межсегментная команда RET или команда IRET, процессор проверяет привилегии селектора сегмента программы, выбираемого из стека (то есть селектора сегмента, которому будет передано управление при выполнении команд RET или IRET). Процессор может выполнить возврат только в менее привилегированный сегмент или в сегмент, обладающий такими же привилегиями, что и тот, из которого выполняется возврат.
к содержанию |