Студопедия

КАТЕГОРИИ:


Архитектура-(3434)Астрономия-(809)Биология-(7483)Биотехнологии-(1457)Военное дело-(14632)Высокие технологии-(1363)География-(913)Геология-(1438)Государство-(451)Демография-(1065)Дом-(47672)Журналистика и СМИ-(912)Изобретательство-(14524)Иностранные языки-(4268)Информатика-(17799)Искусство-(1338)История-(13644)Компьютеры-(11121)Косметика-(55)Кулинария-(373)Культура-(8427)Лингвистика-(374)Литература-(1642)Маркетинг-(23702)Математика-(16968)Машиностроение-(1700)Медицина-(12668)Менеджмент-(24684)Механика-(15423)Науковедение-(506)Образование-(11852)Охрана труда-(3308)Педагогика-(5571)Полиграфия-(1312)Политика-(7869)Право-(5454)Приборостроение-(1369)Программирование-(2801)Производство-(97182)Промышленность-(8706)Психология-(18388)Религия-(3217)Связь-(10668)Сельское хозяйство-(299)Социология-(6455)Спорт-(42831)Строительство-(4793)Торговля-(5050)Транспорт-(2929)Туризм-(1568)Физика-(3942)Философия-(17015)Финансы-(26596)Химия-(22929)Экология-(12095)Экономика-(9961)Электроника-(8441)Электротехника-(4623)Энергетика-(12629)Юриспруденция-(1492)Ядерная техника-(1748)

Связывание программ и данных с адресами в памяти

(слайд №20)

Перед загрузкой данных или кода в память они должны быть в какой-либо момент связаны с определенными адресами в памяти. Связывание может выполняться на разных этапах:

· Связывание во время компиляции (compile-time). Если адрес в памяти априорно известен, компилятором может быть сгенерирован код с абсолютными адресами. При любом изменении размещения программы в памяти должна быть выполнена перекомпиляция. Данный подход более характерен для ранних компьютерных систем с небольшим объемом памяти, либо для обработки и выполнения системных модулей – частей ядра ОС, для которых характерно использование резидентных абсолютных адресов. Для пользовательских программ такой подход неудобен, так как не обеспечивает достаточной гибкости, в частности, возможности без изменений перезагрузить код в другую область памяти.

· Связывание во время загрузки (load-time). Загрузка программы в память – стадия ее обработки системой, предшествующая выполнению программы. Чтобы начальный адрес области памяти, куда загружается программа, можно было менять, и это не привело бы к необходимости изменения кода программы, применяется следующий метод. Генерируется перемещаемый код (relocatable code) – код, в котором адресация происходит относительно значения регистра перемещения (relocation register), и адрес в памяти равен сумме значения регистра перемещения и адреса, вычисляемого в команде. Таким образом, при необходимости загрузки кода на другое место в памяти требуется изменить только значение регистра перемещения. Подобный подход широко используется для программ, написанных на традиционных языках программирования.

· Связывание во время исполнения (runtime), или динамическое (позднее) связывание. Используется, если процесс во время выполнения может быть перемещен из одного сегмента памяти в другой. Для реализации связывания во время исполнения требуется аппаратная поддержка отображения адресов – например, регистры базы и границы. В большинстве систем для пользовательских программ используется, главным образом, именно связывание во время исполнения.

 

-Виртуальная память и стандартные интерфейсы ОС

Вообще, «виртуализация» - это один из краеугольных камней современной вычислительной техники. По правде сказать, «виртуален» и «невещественен» любой компьютер, начиная еще с первых «пентиумов»: ведь, по сути, любая выполняющаяся на них команда, инструкция, операция в той или иной степени виртуальна. Программы работают с виртуальной, а не физической оперативной памятью, процессоры «на лету» перекодируют x86-инструкции в свой внутренний RISC-подобный формат, драйвера устройств и операционные системы прячут под стандартными интерфейсами доступное в системе оборудование. Это зачастую медленно, это почти всегда сложно, но это - единственный способ хоть как-то гарантировать относительную надежность и сравнительную эффективность той чудовищно, непомерно огромной системы, которую мы называем современным компьютером.

Но что же тогда скрывается за модными в последние полгода словами «технологии виртуализации», которые, как уверяют нас гранды процессоростроения, станет не менее весомым аргументом в вопросе покупке нового процессора, чем еще года два назад была возросшая производительность?

У большинства русскоговорящих читателей слово «виртуальный», вопреки его изначальному происхождению, наверное, вызывает примерно одинаковые ассоциации с чем-то невещественным, несуществующим на самом деле. Но изначальный смысл его в вычислительной технике гораздо конкретнее и проще - «виртуальные» объекты здесь всегда означают некие абстрактные интерфейсы, за которыми скрывается реальное оборудование. Основная идея, хорошо прослеживающаяся здесь последние лет двадцать - это стремление максимально упростить задачу разработчикам программного обеспечения, предоставив каждой программе (в идеале) по стандартному «виртуальному компьютеру», на котором она сможет работать без учёта вообще каких бы то ни было сторонних факторов - компьютера, на котором она запущена, или других работающих на этом же компьютере программ. И, надо сказать, результаты здесь были достигнуты впечатляющие. Первые процессоры работали непосредственно с «физической» оперативной памятью, напрямую указывая в программе конкретную ячейку в модуле памяти, с которой они работали. Получалось что-то вроде «модуль памяти #1, микросхема 4, банк 3, строка данных 63, байт 13, - или, в двоичной нотации, «модуль 01, микросхема 01000, банк 11, строка данных 0111111, байт 01101». Эти числа записывались подряд - и получался адрес 010100011011111101101, то есть 669677 в привычной нам десятичной нотации. При соблюдении минимальных ограничений на организацию модулей памяти при таком способе записи фактически получается, что мы нумеруем ячейки памяти идущими подряд числами, начиная с нуля и заканчивая некоторым большим числом. А это удобно и проектировщикам «железа», и программистам. (Кстати, именно отсюда пошло правило «объем модуля памяти должен являться степенью двойки» - при таком подходе все младшие биты физического адреса модуля получаются допустимыми, и в нумерации адресов физической оперативной памяти не возникает «дырок»). Вот с этими «физическими адресами памяти», образующими отрезок на числовой прямой, первые программы и работали. Система была по своему достаточно изящная, но, к сожалению, совершенно не приспособленная для одновременного исполнения нескольких программ, - в лучшем случае одна программа в компьютере могла на время передавать управление другой.

Вообще, о приёмах работы того времени можно составить неплохое впечатление, если вспомнить, что модули привычной нам динамической оперативной памяти (DRAM), требующие регулярной «подзарядки», программисту в те дни приходилось «обновлять» («регенерировать») самостоятельно. Дело в том, что модули DRAM сравнительно быстро теряют хранящуюся в них в виде заряда микроконденсаторов информацию («быстро» здесь означает «за миллисекунды»), и в то время эту особенность данного типа памяти приходилось учитывать «вручную», то есть программными средствами прописывая обращения к ячейкам («столбцам») и тем самым регулярно обновляя хранящуюся в памяти информацию. Лишь позднее функцию регенерации памяти возложили на схемотехнику и микросхемы системной логики (контроллеры памяти) «научились» проводить регенерацию памяти автоматически, в фоновом режиме и незаметно для программиста. Какая уж тогда многозадачность - за регенерацией бы уследить...

Впрочем, сложность, связанная с необходимостью учёта наличия в физической оперативной памяти одновременно нескольких программ (и совершенно разной информации - кода, данных, стека, управляющих структур) - это только полбеды. Главная же беда заключается в том, что в любых программах встречаются различные ошибки (причем, чем сложнее программа, тем этих ошибок больше), очень часто приводящие в первую очередь как раз к порче оперативной памяти по случайному адресу. И «заглючившая» программа, работающая с физической оперативной памятью, в большинстве случаев будет попросту «убивать» не только саму себя (а иногда - и не столько), сколько окружающих её «соседей» по памяти.

(слайд №20)

 

Схема 1. Компьютер без виртуализации

 

Как же разделить и защитить работающие в физической оперативной памяти программы друг от друга? Простейшее решение, которое приходит в голову, - просто «нарезать» эту память («большой отрезок» адресов) небольшими кусочками (меньшими отрезками адресов - сегментами). Каждый такой кусочек задаётся координатами его «начала» (первым адресом отрезка) и «длиной» (количеством адресов). Любой адрес этого отрезка считается как расстояние между этим адресом и первым адресом отрезка («смещение» от начала сегмента). Вместо того чтобы работать с физическими адресами, программы работают с этими смещениями и сегментами, - реализовать это совсем не так уж трудно, причём, выделив каждой программе даже не по одному, а по нескольку сегментов - сегмент для машинного кода программы, сегмент для её данных, сегмент для организации стека, и т.д.

Подобная система называется сегментированной моделью оперативной памяти, в архитектуре x86 она появилась в процессорах i80286 (где получила название «защищенного режима») и исчезла из этой архитектуры только с переходом к 64-битным наборам инструкций AMD64/Intel EM64T.

(слайд №21)

 

Схема 2. Сегментированная память

 

Что мы получаем от перехода к сегментации? Во-первых, защиту одних программ от других: процессор проверяет каждое обращение программ к памяти и контролирует, чтобы они не вышли за пределы выделенных им сегментов. Во-вторых, - и, пожалуй, это даже гораздо важнее - радикальное повышение удобства программирования. Нам не нужно задумываться над тем, что на нашем компьютере вообще существуют другие программы - каждой из них обеспечено «виртуальное» пространство, в котором присутствует даже не одна, а целых три независимых друг от друга «памяти», выделенные ей и только ей, где хранятся ключевые структурные части любой запущенной программы - код, данные и стек. Нам не нужно подстраиваться под особенности конкретной версии операционной системы (а это ведь тоже как минимум еще одна программа, запущенная на компьютере!), мы можем использовать для всех случаев жизни совершенно одинаковый программный код.

Эффективная схема? Вполне! Она работает, она удобна, доступна и понятна, так что многие любители ассемблера до сих пор с удовольствием ею пользуются. Но долго она не продержалась, поскольку, как легко догадаться, особенной гибкостью в использовании не отличается. Как мы изначально «нарежем» память ломтиками, - так оно в будущем и останется: выделим слишком много - какие-то области останутся неиспользованными и простаивающими; выделим слишком мало - не сможем в нужный момент увеличить этот объём. Помнят ли еще программисты старые добрые DOS-овские среды разработки от Borland, где в опциях компилятора указывалась «модель памяти», в которой определялся размер и количество используемых в программе сегментов? И помнят ли пользователи замечательную утилитку mem и знаменитое Not enough memory, которыми так радовали глаз пользователя ранние «персоналки»?

Одним словом, даже в те времена существовали лучшие решения, и в следующем, первом по-настоящему современном поколении x86-х процессоров (80386) вслед за процессорами Motorola и мэйнфреймами появилась основа любых современных многозадачных ОС - виртуальная память. Об удачности этой разработки говорит хотя бы то, что вплоть до перехода к 64-битным наборам инструкций «ядро» любых x86 в точности соответствовало стандарту IA-32 (Intel Architecture for 32-bit), введённому Intel для i386 (так что, в принципе, на «трёшках» должны работать любые 32-битные программы, не задействующие слишком современных функций).

Виртуальная память (схема 3) - это логическое развитие идеи сегментированной памяти, когда мы переходим от вполне конкретным образом преобразуемых в физические «линейных» адресов защищенного режима x86 к совершенно абстрактным «виртуальным» адресам. Ведь, по большому счёту, для работающей на компьютере программы совершенно безразлично, что за «физические» ячейки памяти она использует! Ей нужен просто некоторый диапазон адресов, по которым она сможет сохранять свои данные, а что за этими «цифирками» на самом деле скрывается, ей глубоко безразлично. Главное - чтобы процессор знал, как эти абстрактные цифры (виртуальные адреса) переводить во вполне конкретные инструкции для контроллера памяти (физические адреса).

(слайд №22)

 

 

Схема 3. Виртуальная память

 

Как это делается на практике? Вся доступная процессору физическая оперативная память разбивается на небольшие кусочки размером 4 Кбайт или 4 Мбайт - «страницы». При этом используется та же схема, что и при разбивке физических адресов на адреса конкретной ячейки памяти: младшие 12 или 22 бит виртуального адреса обозначают смещение данного адреса от начала страницы, а старшие биты (от 10 до 50) - номер страницы. Когда процессору требуется вычислить физический адрес по виртуальному, он просто разделяет виртуальный адрес на номер страницы и смещение, заглядывает в таблицу, где для каждого номера указаны координаты начала страницы в физической памяти, и прибавляет к полученной координате смещение (схема 4).

Упомянутая табличка страниц называется таблицей трансляции адресов виртуальной памяти (или просто таблицей трансляции), и размещается она в виде B-дерева в самой обыкновенной оперативной памяти, что позволяет создавать без большой избыточности сколь угодно большие быстродействующие таблицы трансляции. Работает это дерево, правда (как и всё, связанное с оперативной памятью), по-прежнему не очень быстро, и поэтому процессор кэширует ранее определенные соответствия «номер страницы - запись в таблице трансляции» в специальном кэше - буфере трансляции виртуальных адресов (Translation Look-aside Buffer, TLB).

 

 

Схема 4. Работа с виртуальной памятью.

 

 

Кстати, сегментация (в 32-битных процессорах) даже с виртуальной памятью всё равно сохраняется. То есть реальные адреса, упоминающиеся в программе, вначале превращаются с учётом сегментов в «линейные», а уже они с помощью таблицы трансляции - в реальные «физические» адреса.

Трудно поверить, но, казалось бы ничем глубоко принципиальным не отличающаяся от обычной сегментированной модели памяти, память виртуальная даёт системному программисту практически всё, чего только его душа пожелает. Дело в том, что собственно усовершенствованной «трансляцией» адресов (которая сама по себе снимает все проблемы сегментированной оперативной памяти) виртуальная память не ограничивается. Вся «соль» технологии - в том, что для каждой записи в таблице трансляции адресов (фактически - для каждого диапазона адресов виртуальной памяти) определен набор специальных флагов, которые реализуют:

· Защиту важных участков оперативной памяти от перезаписи.
Один из простейших «флажков», который указывается для адресов виртуальной памяти - это флажок «только для чтения», позволяющий защитить определенные области виртуальной оперативной памяти от записи. К примеру, обычно read-only объявляются страницы, содержащие машинный код программы.

· Защиту программ от вирусов.
Основа новомодных «антивирусных» технологий вроде Microsoft Data Execution Prevention, обеспечивающих надёжную защиту компьютера от эксплойтов, использующих атаки типа «переполнение буфера», - крошечный битик в таблице трансляции (No eXecute - NX у AMD, и eXecute Disable, XD - у Intel), запрещающий выполнение машинного кода из определенных участков памяти.

· Защиту операционной системы.
Специальный бит позволяет определить некоторые участки оперативной памяти как «системные» и принципиально недоступные обычному приложению как для чтения, так и для записи.

· Эффективный менеджмент оперативной памяти.
Целый ряд специальных битов позволяет операционной системе отслеживать, по каким адресам программа читала или записывала данные, и определить «глобальные» страницы памяти, общие для всех программ в процессоре.

Но самый главный бит в таблице трансляции - это «нулевой» бит P - Present, обеспечивающий собственно

 

 

-Паравиртуализация и бинарная трансляция

Итак, как мы уже сказали, все пользовательские приложения сегодня, фактически, работают на «виртуальных» компьютерах - им предоставляется некая «обобщенно-стандартная» среда исполнения с виртуальной оперативной памятью, и с этим «виртуальным компьютером» они свободно работают, не задумываясь о том, какие реальные физические ресурсы за этой виртуальностью стоят. Центральная задача операционной системы - это поддержание этой «виртуальной реальности» и своевременное распределение между этими виртуальностями реальных аппаратных ресурсов. Сама операционная система тоже живёт на одном из «виртуальных компьютеров», но, в отличие от всех остальных «обитателей» компьютера, обладает возможностью свою (и чужие) «реальности» изменять и соотносить с физическими ресурсами компьютера.

И уже сама по себе подобная возможность позволяет, на самом деле, реализовывать практически всё, что угодно, с пользовательскими приложениями. К примеру, потенциально можно взять, «сохранить» состояние приложения на флэшку, «скопировать» на другой компьютер и «продолжить» выполнение программы уже на другом компьютере. Можно (потенциально) запускать в одной и той же операционной системе как Windows, так и POSIX-приложения (Linux, Unix-системы) - достаточно уметь создавать два «типа» виртуальных компьютеров, чтобы каждое приложение получало ровно ту среду исполнения, в которой оно привыкло работать. Но, к сожалению, для пользователя, подобные «хитрости», требующие активной поддержки со стороны операционной системы, реализовать на практике далеко не так просто, как рассказать о них. И обеспечить, скажем, «родную» поддержку Windows-приложений в Linux, равно как и обратную поддержку Linux-приложений в Windows, по причине активного противодействия Microsoft, невозможно. А потому пользователь вынужден обходиться без некоторых интересных функций и довольствоваться Windows-приложениями на Windows-системах и Linux-приложениями на Linux-системах.

В качестве выхода из ситуации возникает вполне логичное предложение: если уж мы не можем объединить в одной операционной системе возможности нескольких разных ОС, то почему бы одновременно запустить на нашем компьютере не одну, а сразу несколько операционных систем? Заодно и надёжность повысим: если одна из операционных систем «упадёт», другая останется, и будет способна восстановить «упавшую».

Оказывается, что это не столь уж трудно сделать. Смотрите: наши операционные системы - по сути дела, те же самые обычные приложения, работающие с виртуальными компьютерами, но разве что наделённые чуть более широкими привилегиями и потому обладающие способностью «трансформировать» окружающую среду под свои нужды. Поэтому возможны целых два способа обеспечить одновременную их работу на одном и том же компьютере.

Способ первый - это «способ сознательного сотрудничества»: сводящийся к тому, что наши ОС будут «учитывать интересы» друг друга, распределят между собой аппаратные ресурсы, и впредь будут работать так, чтобы не навредить своими «чрезвычайными полномочиями» операционной системы другой системе. Подобный подход весьма широко практикуется в *nix-подобных операционных системах и называется паравиртуализацией. Однако поскольку данный способ требует серьезной модификации ядра ОС, на которое, к примеру, всё та же Microsoft, доминирующая на рынке операционных систем, естественно, не соглашается, то особенной популярности среди «обычных пользователей» он получить не сумел.

 

 

Схема 5. Паравиртуализация

 

Второй способ очень хорошо знаком «продвинутым пользователям» по приложениям типа VMWare Workstation, обеспечивающим успешный запуск на одном компьютере из-под «базовой» операционной системы нескольких «гостевых» операционных систем без специальной их модификации. «Гостевая» операционная система вместе со всеми её приложениями фактически становится одним «обычным» приложением «родительской» операционной системы, из-под которой она запущена. Идея здесь очень простая: используя виртуальную память, мы можем сымитировать виртуальный компьютер практически любой сложности: так что «гостевой» операционной системе попросту «подсовывается» виртуальная машина, очень напоминающая «физическую» x86-машину. «Гость» принимает «обманку» за настоящий компьютер - и вполне успешно начинает на этой виртуальной машине, имитируемой «родительской» ОС, работать. Обратите внимание, на то, что это не подход, аналогичный «виртуальной машине Java» или эмуляторам древнего Sinclair, когда приложение-эмулятор виртуальной машины «вручную» разбирает код приложения и «вручную» же исполняет каждую его инструкцию. Гостевая операционная система и все запущенные в её рамках приложения работают на физических ресурсах компьютера практически так же, как это делает обычное запущенное на нём приложение, а «виртуализирующее приложение» только обеспечивает контроль над ним - тонюсенькая прослойка кода, поддержанная стандартными аппаратными ресурсами компьютера. Давайте разберём немножко подробнее, как такое оказывается возможным.

У нас есть некие аппаратные ресурсы, которые нужно имитировать. В архитектуре x86 их, в общем-то, всего три:

· Регистры процессора (включая регистры служебного назначения).

· Порты ввода-вывода (использующиеся для обмена информацией с периферией).

· Оперативная память.

 

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

Но есть несколько неприятных исключений. Вот, к примеру, уже упоминавшийся регистр CR3, управляющий таблицей трансляции оперативной памяти. Собственно, зная «виртуальное» значение CR3, «базовой» операционной системе нетрудно сымитировать собственно таблицу трансляции: достаточно относящиеся к этой таблице области виртуальной памяти пометить при помощи P-флага, получить таким образом перехват всех обращений к этой таблице, и синхронизировать реальную таблицу трансляции с виртуальной, которую гостевая операционная система принимает за реальную (техника «теневых таблиц трансляции», Shadow Page Table). Но при этом, к сожалению, нужно как-то обманывать гостевую операционную систему, «подсовывая» ей «виртуальный CR3» вместо реального, а средств соответствующего аппаратного контроля обычный x86-процессор не предоставляет.

 

 

Схема 6. Виртуализация с гостевыми ОС.

 

Еще одна проблема из той же серии - внутренний регистр процессора, отвечающий за «уровень привилегий» текущего запущенного приложения. Процессор использует его, чтобы перехватывать попытки обращения «обычных» приложений к «опасным», «недозволенным» инструкциям и областям памяти; назначается этот уровень привилегий операционной системой. Таких уровней всего четыре; о приложениях с заданным уровнем привилегий говорят, что они работают в соответствующем кольце. Чем меньше численное значение данного параметра, тем больше дозволено соответствующим приложениям. В кольце 0 (Ring 0), к примеру, работает операционная система и (обычно) драйвера операционной системы; в кольце 3 (Ring 3) - «обычные» пользовательские приложения. Так вот: доверять «гостевой» операционной системе нулевое кольцо нельзя - иначе невозможно будет перехватывать некоторые её действия, поскольку в нулевом кольце «дозволено всё» и многие проверки безопасности попросту не работают. Но поскольку гостевая операционная система, естественно, по умолчанию предполагает, что её нужно запускать именно в нулевом кольце, а проверить сей факт особенного труда не представляет, то вполне естественно, что при попытке её запуска в каком-либо другом кольце приложение-виртуализатор добьётся разве что сообщения об ошибке. Поэтому, строго говоря, полноценную имитацию «физического» компьютера с помощью аппаратных ресурсов виртуализации в x86 нельзя. Говорят, что не выполнен критерий самовиртуализируемости Попека и Голберга (Popek and Goldberg self-virtualization requirements).

Как же тогда работают «виртуализаторы» типа VMWare? Довольно нетривиальным образом. Виртуализатор слегка «подрезает крылья» коду выполняющейся под его управлением операционной системы, на лету дизассемблируя её код и заменяя «плохие» инструкции (вроде чтения-записи регистра CR3) нейтральными с её точки зрения (это называется динамической трансляцией; dynamic recompilation). Сделать это, мягко говоря, не так уж просто, а гарантировать работоспособность получающегося на выходе результата - еще сложнее. Приплюсуйте сюда задачку имитации софтом виртуального x86-компьютера (требующую реализации специального сложнейшего драйвера), и вы получите представление о том, почему «виртуализирующее ПО» для x86 до сих пор не отличалось ни особенной надёжностью, ни особенной производительностью. Увы, но в архитектуре IA-32 с её изначально неплохой виртуализационной функциональностью изначально была заложена здоровенная «дырка», которую возможно обойти только с большим трудом.

<== предыдущая лекция | следующая лекция ==>
Стратегии выталкивания | Покупательское поведение
Поделиться с друзьями:


Дата добавления: 2014-01-05; Просмотров: 814; Нарушение авторских прав?; Мы поможем в написании вашей работы!


Нам важно ваше мнение! Был ли полезен опубликованный материал? Да | Нет



studopedia.su - Студопедия (2013 - 2024) год. Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав! Последнее добавление




Генерация страницы за: 0.037 сек.