Журнал Компьютерра - Журнал «Компьютерра» №32 от 06 сентября 2005 года
Рассмотрим суть возникающих неприятностей на примере так называемой проблемы нулевого кольца исполнения. Кольца (rings, они же уровни приоритета - PLs, Priority Levels) - это такая хитрая система, защищающая центральный процессор от выполнения «посторонними» программами «глубоко системных» инструкций и операций, эдакий «уровень доступа» запущенной программы к системным ресурсам. В IA-32 четыре кольца, от Ring 0 до Ring 3. Чем больше номер кольца - тем меньше приоритет и тем меньше доступно работающей программе. В нулевом кольце запущенный процесс может делать все, что заблагорассудится, - ему предоставлен карт-бланш на любые операции; так что именно в этом кольце «обитает» ядро операционной системы и непосредственно взаимодействующие с оборудованием драйверы. Третье кольцо - сильно упрощенный и ограниченный мирок, в котором запущенный процесс может свободно «жить», но изменить который он не в состоянии. Здесь «живут» всяческие прикладные программы. Кольца 1 и 2 ни Windows, ни *nix-системами принципиально не используются.
В чем же проблема? Оказывается, когда мы «дурачим» виртуальную операционную систему, то не совсем понятно, в какое из колец ее следует помещать. В Ring 0, очевидно, поместить ОС нельзя - проконтролировать ее действия в этом случае не сумеет ни одна живая душа, ибо «воля» исполняющегося в этом кольце кода обсуждению или критике со стороны процессора не подлежит. Поместить операционную систему в «непривилегированные» первое, второе или третье кольца тоже нельзя: любая ОС проектируется, исходя из расчета, что выполняться она будет в нулевом кольце привилегий, а значит, отсутствия необходимого минимума маленьких, но жизненно важных для работы ее ядра инструкций не простит. Вот и приходится программистам либо заблаговременно проверять код на предмет «неблагонадежных» инструкций, ставя на их место вызовы «правильных заменителей», либо отлавливать возникающие при исполнении «неправильных» инструкций ошибки и пытаться их на лету исправлять. Легко догадаться, что реализация и того и другого выливается в большую головную боль для программистов, причем на вроде бы совершенно пустом месте. К примеру, часто «гостевую» операционную систему размещают в неиспользуемом первом кольце… но в расширенном 64-битном режиме процессор не поддерживает кольца 1 и 2, так что все соответствующие наработки «виртуализатор», естественно, отправляет в корзину. Рано или поздно количество проблем переходит в качество - и виртуальные машины становятся не только крайне сложными с программной точки зрения, но и громоздкими, медленными и ненадежными.
***
Искусство обмана. Intel Vanderpool
Так что же делать? Отказаться от надежды на массовый виртуальный ПК? Конечно, нет! Как минимум, вместо того, чтобы заниматься замысловатой оптимизацией кода для хитроумного обмана операционной системы, можно просто внести некоторые изменения собственно в ОС, видоизменив «наиболее мешающиеся» части ядра. Подобный подход называется паравиртуализацией, он активно продвигается компанией Sun и поддержан движением OpenSource… но эту красивую картинку, к сожалению, успешно портит своими «незаменимыми продуктами» великая и ужасная Microsoft, ибо адаптировать сверхпопулярное ядро Windows NT к реалиям конкретной виртуальной машины может, естественно, только его автор; но так, как делать этого софтверный гигант не собирается (слишком уж многое нужно перелопачивать в ядре), то и сам подход получается если не тупиковым, то, по крайней мере, неполноценным. Поэтому куда интереснее второй, гораздо более простой и универсальный способ - аппаратная поддержка функционирования менеджеров виртуальных компьютеров со стороны процессора, то есть технологии виртуализации. Именно так и поступила Microsoft, разработав в тесном сотрудничестве со специалистами VMWare систему, снимающую основную «головную боль» с разработчиков соответствующего ПО.
Центральная идея Intel Virtualization Technology (ранее известной под названием Vanderpool) - введение в процессор аппаратной поддержки некой специализированной программы - менеджера виртуальной машины (VMM, Virtual Machine Manager) (рис. 1). В принципе, VMM - по всем статьям обычная программа, работающая на персональном компьютере. Однако «права» у этой «обычной» программы самые невероятные, поскольку она может вмешиваться во все мало-мальски значимые события, происходящие в процессоре. То есть, если, скажем, ядро операционной системы (работающее в обычном, ничем не ограничиваемом Ring 0) вызывает инструкцию CPUID, или читает-записывает важный системный регистр CR[Что, впрочем, неудивительно - AMD сегодня отвоевывает себе «место под рыночным солнцем», а для этого ей необходимо предлагать более совершенные и интересные решения], или произошло какое-то внешнее событие, вроде прерывания - то процессор сообщает об этом VMM и… больше ничего не делает, предоставляя VMM право реагировать на возникшее событие самостоятельно. Менеджер виртуального ПК «разбирается» в происходящем, решает, как поступить (например, может, необходимо после нажатия какой-то кнопки переключить CPU на другую запущенную операционную систему), и «программно» имитирует действия центрального процессора в подобной ситуации. Причем отслеживать разные события ему приходится довольно интенсивно: «вручную» маршрутизировать обращения операционных систем к периферийным устройствам, «вручную» загружать и переключать запущенные операционные системы, «вручную» следить за виртуальной памятью запущенных операционных систем, чтобы последние нечаянно не «покалечили» друг друга, считая себя единственными претендентами на имеющиеся физические ресурсы.
Последний пример хорошо иллюстрирует суть происходящего. Как известно, современные многозадачные операционные системы активно используют механизм виртуальной памяти, когда запущенные на процессоре программы работают с «линейными» адресами, произвольным образом отображенными в реальную физическую оперативную память (или еще куда-нибудь - например, на участок жесткого диска в своп-файле или своп-разделе). С физической памятью не работает даже сама операционная система: слишком уж это неудобно - подстраиваться под особенности конкретного ПК, и слишком опасно - когда в общей куче перемешаны данные десятков и сотен запущенных программ, и только аккуратность разработчика защищает эти данные от ошибочных действий программного обеспечения. Преобразование виртуальных «линейных» адресов в физические обычно выполняет сам центральный процессор, для чего у него предусмотрена специальная таблица трансляции адресов, в которой записано, какому «линейному» адресу какой физический соответствует (или стоит пометка, что при обращении к данному участку памяти процессору следует позвать на помощь операционную систему). Хранится эта таблица не в регистрах процессора, а в оперативной памяти (в виде трех-четырехуровневого B-дерева, если вас интересуют подробности), причем таблиц может быть сколько угодно: для переключения к другому виртуальному адресному пространству в процессоре достаточно изменить один-единственный регистр CR3 (указатель на таблицу трансляции) и выполнить специальную команду сброса и очистки кэшей CPU, в которых может сохраняться старая информация о виртуальной памяти. «Простым смертным» в лице обычных программ доступ к CR3, разумеется, закрыт, и поэтому изменить «мир», в котором программы находятся, они могут только путем обращения за помощью к операционной системе. Операционной системе «проще» - она, с одной стороны, подчиняется «общим правилам», а с другой - может эти правила самостоятельно «переписывать», работая ровно в тех условиях, которые кажутся ей наиболее приемлемыми. Скажем, если ОС срочно потребуется записать что-то по физическому адресу 0x00123456, она вначале создаст в таблице трансляции своих виртуальных адресов ссылку на соответствующий участок оперативной памяти (отобразив его в то место виртуального линейного пространства ОС, которое покажется ей самым удобным) и затем обратится по свежесозданному виртуальному адресу. Для «обычного» же процесса (который, строго говоря, почти ничем не отличается от процесса ядра операционной системы) подобная «лазейка» к, скажем, «соседям» по компьютеру закрыта - ему просто-напросто не дадут увидеть собственную таблицу трансляции, выведя ее за пределы «видимости» виртуального пространства его памяти либо защитив эту область памяти от записи (здесь тоже используется та самая таблица трансляции - в ней можно указывать права доступа к виртуальным адресам). Все замечательно, все удобно, никаких проблем.
Но стоп, - скажет здесь читатель, - а что же будет, если мы запустим две операционные системы и одна из них решит отобрать для своих целей кусочек физического, а не виртуального пространства памяти другой операционной системы, отобразив его в своей таблице трансляции? В обычных условиях это фатальная ошибка, которая быстро приведет две совместно работающие операционки к краху. Но если у нас есть VMM, то ситуация в корне меняется, ибо мы теперь можем запущенную операционную систему «одурачить», перехватив соответствующее обращение и подсунув ей не настоящий CR3, а специально подготовленную «пустышку». ОС, наивно полагая, что она полностью контролирует виртуальную память компьютера, на самом деле будет всего лишь изменять никак не связанную с настоящей таблицей трансляции область оперативной памяти. А обращения к ней, используя стандартные же механизмы виртуальной памяти, будет перехватывать все тот же VMM и синхронизировать в соответствии с ними настоящую таблицу трансляции. Красиво придумано, не правда ли? Собственно виртуальной машины у нас нет, просто мы тщательно контролируем работу операционной системы и при необходимости ловко манипулируем ею.