Сегменты, страницы и системные вызовы
Системы семейства Unix используют х86 как нормальную 32-разрядную машину с двухуровневым доступом: пользовательской задаче выделяется один сегмент, ядру– другой. 4-х гигабайтового сегмента х86, разбитого на страницы размером по 4 Кбайт, достаточно для большинства практических целей. Например, в Linux системный вызов исполняется командой int 8Oh. Селектор пользовательского сегмента помещается в регистр FS. Для доступа к этому сегменту из модулей ядра используются процедуры memcpy_from_fs и memcpy_to_fs.
Многоуровневый доступ, основанный на концепции колец, не имеет принципиальных преимуществ по сравнению с двумя уровнями привилегий. Как и в двухуровневой системе, пользовательские модули вынуждены полностью доверять привилегированным, привилегированные же модули не могут защититься даже от ошибок в собственном коде. Самое лучшее, что может сделать Windows XP, обнаружив попытку обращения к недопустимому адресу в режиме ядра, – это нарисовать на синем экране дамп регистров процессора. В OS/2, фатальная ошибка в привилегированных модулях, исполняемых во втором кольце защиты, не обязательно приводит к остановке ядра, но подсистема, в которой произошла ошибка, оказывается неработоспособна. Если испорчен пользовательский интерфейс или сетевая подсистема, система в целом становится бесполезной и нуждается в перезагрузке.
Кроме того, разделение адресных пространств создает сложности при разделении кода и данных между процессами: разделяемые объекты могут оказаться отображены в разных процессах на разные адреса, поэтому в таких объектах нельзя хранить указатели (подробнее см. разд. "Разделяемые библиотеки"). Стремление обойти эти трудности и создать систему, в которой сочетались бы преимущества как единого (легкость и естественность межпроцессного взаимодействия), так и раздельных (защита процессов друг от друга) адресных пространств, многие годы занимало умы разработчиков аппаратных архитектур.
Например, машины фирмы Burroughs предоставляли пользователю и системе единое адресное пространство, разбитое на сегменты (единую таблицу дескрипторов сегментов). При этом возникает вопрос: если все задачи используют одну таблицу, как может получиться, что разные задачи имеют разные права на один и тот же сегмент?
Решение этого вопроса состоит в том, что права доступа кодируются не дескриптором, а селектором сегмента. Таким образом, разные права доступа на один сегмент – это, строго говоря, разные адреса. Понятно, что такое разделение работает лишь постольку, поскольку пользовательская программа не может формировать произвольные селекторы. В компьютерах Burroughs это достигалось теговой архитектурой: каждое слово памяти снабжалось дополнительными битами, тегом (tag), который определял тип данных, хранимых в этом слове, и допустимые над ним операции. Битовые операции над указателями не допускались. Благодаря этому задача не могла сформировать не только произвольные права доступа, но и вообще произвольный указатель.
Аналогичным образом реализована защита памяти в AS/400 [redbooks.ibm.com sg242222.pdf). Пользовательские программы имеют общее адресное пространство. Первоначально это общее пространство имен, в процессе же преобразования имени в адрес бинарное представление селектора сегмента снабжается и битами доступа, которые затем обрабатываются точно так же, как в машинах Burroughs, и точно так же недоступны пользовательскому коду для модификации.
Реализации языка С для этой архитектуры допускают использование указателей только в пределах одного сегмента кода или данных, но не позволяют формировать средствами С произвольные селекторы сегментов, остальные же языки, используемые на этой платформе (Cobol, RPG, языки хранимых процедур СУБД) вообще не имеют указателя как понятия.
Еще дальше в том же направлении продвинулись разработчики фирмы Intel, создавая экспериментальный микропроцессор 1АРХ432, описанный в следующем разделе.