- Адресная строка Проводника
- Адресное пространство процесса в Windows
- Адресация на уровне процессора
- Механизм трансляции адресов (преобразование адреса)
- Механизм трансляции страниц (страничное преобразование)
- Адресация на уровне ОС
- Размерность адресных пространств
- Физическое адресное пространство
- Линейное адресное пространство
- Использование страничной организации операционной системой
- Структура виртуального адресного пространства процесса
- Размещение в адресном пространстве структур и библиотек
- Выводы
Адресная строка Проводника
Данный элемент интерфейса операционной системы Windows играет важную роль в удобстве работы на компьютере. Именно по этому, разработчики не обходят ее своим вниманием и по сравнению с Windows XP, начиная с 7 она несколько видоизменилась и обрела новую функциональность.
Кто то из читателей не узнает для себя ничего нового, а кто-то возможно откроет для себя новые способы эффективной работы на компьютере. Давайте рассмотрим особенности использования адресной строки Проводника в Windows 7/8/10, а так же затронем вопросы приватности наших перемещений.
Главное новшество по сравнению с Windows XP состоит в том, что теперь в адресной строке показывается путь в виде отдельных папок разделенных треугольником. Это сделано не для красоты, а для быстрой и удобной навигации по файловой системе компьютера. Когда вы наводите курсор мыши на элемент, он подсвечивается и если на него нажать, то вы переместитесь сразу в это место на жестком диске. Теперь чтобы переместиться на 3 уровня вверх достаточно одного щелчка мыши. Кстати, простой способ открыть папку в новом окне Проводника описан здесь.
Однако это еще не все, нажав на разделяющий узлы треугольник, появится контекстное меню со списком вложенных папок для данной папки. Другими словами, теперь можно не только быстро перейти на несколько уровней вверх, но и сразу переместиться в любую другую папку или корень логического диска.
Это конечно хорошо, но что делать, когда вам нужно скопировать текущий путь в буфер обмена. На самом деле все очень просто и это можно сделать даже несколькими путями. Можно щелкнуть мышью по свободному месту в адресной строке Проводника Windows 7, она сразу преобразится в обычный вид знакомый нам еще по XP. Теперь копируем путь с помощью мыши или комбинации горячих клавиш. Впрочем, это можно сделать и несколько проще, достаточно щелкнуть по адресной строке правой кнопкой мыши в появившемся меню выбрать пункт «Копировать адрес как текст» и он окажется в буфере обмена.
Вы наверно уже заметили, что там есть еще вариант «Копировать адрес» и возможно даже попробовали его выбрать. Наверняка результат вас несколько удивил. Дело в том, что название данного пункта меню не совсем соответствует выполняемым им действиям. Фактически копируется вся папка со всем содержимом, а не только путь до нее, поэтому конечный результат будет зависеть от того, куда вы попытаетесь её затем вставить. Наиболее логично вставлять в окно Проводника (не в адресную строку). Пункт меню «Изменить адрес» равноценен щелчку ЛКМ по свободному месту адресной строки Проводника. Сейчас, как и раньше, адрес можно набирать вручную с клавиатуры.
Внимательные пользователи компьютера наверняка обратили внимание, что справа в конце строки есть еще один треугольник, направленный вниз. Щелчок по нему открывает список с путями, введенными в адресную строку Проводника Windows, что позволяет снова быстро по нему перейти. Это удобно, но иногда хочется внести в него какие то изменения или вовсе удалить.
Очистить этот список очень просто, достаточно выбрать последний не рассмотренный нами пункт меню «Удалить журнал». Несколько сложнее сделать выборочное редактирование этого списка, поскольку графический интерфейс такой возможности не предоставляет. Для этого нам придется воспользоваться редактированием реестра. История посещений Проводника для данной учетной записи хранится по адресу: HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\TypedPaths .
Как можно быстро перейти к этому разделу реестра рассказано здесь. История хранится в виде имя параметра и его значение. Их можно удалять, редактировать или добавлять новые. Цифра в имени параметра (например, url3) обозначает порядковый номер ссылки в списке, причем нумерация не должна прерываться. Если просто удалить параметр с номером из середины списка, все следующие за ним перестанут отображаться. Поэтому просто взять и удалить ненужную запись не получится, если только она не последняя.
Чтобы увидеть сделанные изменения в Проводнике необходимо закрыть все его окна. Новые пути добавленные через Проводник появятся в списке после закрытия всех его окон в начало списка. В редакторе реестра, если он отрыт нужно нажать клавишу F5 .
Теперь вы в курсе, как можно быстро перемещаться по файловой системе с помощью адресной строки Проводника, а так же отредактировать или полностью очистить историю посещений в адресной строке Проводника.
Адресное пространство процесса в Windows
Я думаю, не будет слишком смелым заявление о том, что одним из основополагающих моментов в изучении работы операционной системы является понимание принципа управления оперативной памятью. Мало найдется желающих оспаривать утверждение, что оперативная память (ОЗУ) является тем компонентом персональных компьютеров, важность которого, при современной архитектуре вычислительных систем, сложно переоценить, и без которого работа их (в силу архитектурных особенностей) представляется невозможной. Мне всегда хотелось понять, как именно ОС управляет доступной ей памятью? Как она распределяет её между загруженными приложениями? Как происходит организация (создание) в памяти нового процесса, как код программы получает управление и как процессу выделяется дополнительная память по запросу, в случае, когда выделенная изначально память заканчивается? Как организовано адресное пространство процесса? Подобные вопросы возникали и продолжают возникать у меня довольно часто, но я так же часто замечаю, что далеко не на все из них у меня находятся вразумительные ответы.
Поскольку круг вопросов, касающихся оперативной памяти настолько велик, что не может быть освещен в одной статье, здесь мы коснемся лишь части огромного механизма управления памяти в ОС Microsoft Windows, а именно изучим адресное пространство процесса, увидим, что же размещается [системой] в пространстве памяти, выделяемой процессу.
Любые программы (код), исполняемые на процессоре, оперируют данными. Данные, в свою очередь, могут располагаться в оперативной памяти, которую легче всего себе представить в виде массива байт. Если данные размещены в памяти, то их положение нужно каким-либо образом определить — проще всего сделать это при помощи некоего числа (порядкового номера), называемым адресом [в массиве]. Чтобы обратиться к конкретному байту данных (массива), или адресовать его, логично было бы использовать этот самый порядковый номер. Подобным образом можно поступить со всеми байтами, пронумеровав их целыми положительными числами, установив ноль за начало отсчёта. Индекс байта в этом огромном массиве и будет его адресом.
Адресация на уровне процессора
В первых микропроцессорах компании Intel (архитектуры x86 ) был доступен единственный режим работы процессора, впоследствии названный реальным режимом . Адресация памяти в процессорах того времени была достаточно простой и носила название сегментной . Суть её заключалась в том, что ячейка памяти адресовалась при помощи двух составляющих: сегмент : смещение (сегмент — область адресного пространства фиксированного размера, смещение — адрес ячейки памяти относительно начала сегмента). Специфика архитектуры упомянутых [первых] микропроцессоров накладывала ограничения на размер физического адресного пространства (16 килобайт, 64 килобайта, 1 мегабайт. ), и память, доступная программно, была не более размера оперативной (физически установленной) памяти компьютера, на котором она выполнялась. Это было просто, логично и понятно. Тем не менее, подобная архитектура накладывала и определенные неудобства:
- Была актуальна проблема согласования выделения памяти различным приложениям. Размещение кода/данных приложений в едином для всех программ пространстве памяти требовало от операционной системы (а иногда и от самой программы) сложного механизма постоянного отслеживания занятого пространства.
- Наметился переход к многозадачным операционным системам, в которых большое количество задач выполнялось псевдопараллельно, что затрудняло использование общего пространства памяти.
- Еще одной проблемой была безопасность и необходимость ограничения доступа к «чужим» процессам в памяти.
Эти, а так же некоторые другие, проблемы явились отправной точкой для работы над усовершенствованием, в следствии чего в процессоре 80286 появился защищенный режим и концепция сегментной адресации памяти была значительно расширена для обеспечения новых требований. Например в защищенном режиме сегменты могли располагаться (начинаться) в памяти в произвольном месте (база), иметь нефиксированный размер (лимит), уровни доступа, типы содержимого и прочее. И наконец, по прошествии некоторого времени была создана новая архитектура, получившая название IA-32 , в которой были введены несколько новых моделей организации оперативной памяти:
- Базовая плоская модель (basic flat model) — наиболее простая модель памяти [системы], операционная система и приложения получают в своё распоряжение непрерывное, не сегментированное адресное пространство.
- Защищенная плоская модель (protected flat model) — более сложная модель памяти [системы], может применяться страничный механизм изоляции пользовательского/системного кода/данных, описываются четыре сегмента: кода/данных (для уровня привилегий 3, пользовательский уровень) и кода/данных (для уровня привелегий 0, ядро).
- Мульти-сегментная модель (multi-segment Model) — самая сложная модель памяти [системы], предоставляется аппаратная защита кода, данных, программ и задач. Каждой программе (или задаче) назначаются их собственные таблицы [сегментных] дескрипторов и собственные сегменты.
Изменения коснулись и принципов адресации: если в реальном режиме работы процессора пара сегментный_регистр : смещение могла адресовать ячейку памяти, то в защищенном режиме сегмент был заменен на селектор, который содержал индекс в таблице дескрипторов и биты вида таблицы дескрипторов + биты привилегий.
Механизм трансляции адресов (преобразование адреса)
Одним из основных нововведений защищенного режима было создание механизма трансляции виртуальных адресов в физические и механизм трансляции страниц, благодаря которым операционная система имеет возможность создать иллюзию обособленного адресного пространства (для каждого процесса) и защитить собственное ядро от пользовательских программ. Адрес, по которому код программы (приложения) пытается обратиться к данным (где-то в оперативной памяти), изначально закодирован в исполняемой инструкции (пример):
..и проходит через некоторое количество преобразований, начиная от декодирования инструкции процессором и заканчивая выставлением адреса на шину. Давайте посмотрим, какие же этапы проходит преобразование адреса:
- Эффективный адрес — адрес, задаваемый в аргументах машинной инструкции при помощи регистров, смещений, коэффициентов. Фактически эффективный адрес представляет собой смещение от начала сегмента (базы). Как раз для нашего примера (выше): эффективный адрес = EDI + ECX * 4 + 4 ;
- Логический адрес – адрес, представляющий собой пару селектор : смещение . Традиционно селектор (левая часть) располагается в сегментном регистре, смещение (правая часть) в регистре общего назначения или указывается непосредственно, для нашего примера это: DS : [EDI+ECX*4+4] . Как мы видим, часто сегментный регистр (левая часть) не указывается (выбирается неявно). Фактически с логическими адресами и имеет дело программист в своих программах;
- Линейный адрес — это 32-/64-разрядный адрес, получаемый путем использования селектора (содержащегося в левой части виртуального адреса, в сегментном регистре, для нашего примера — в DS ) в качестве индекса в таблице дескрипторов (для вычисления базы сегмента) и добавления к ней смещения (правая часть виртуального адреса, в нашем случае значение, вычисляемое на основе выражения EDI+ECX*4+4 ). Линейный адрес = база сегмента + эффективный адрес .
- Гостевой физический адрес — при использовании аппаратной виртуализации. В случае, когда в системе работают виртуальные машины, физические адреса (получаемые в каждой из них), необходимо транслировать ещё раз.
- Физический адрес — это финальная часть преобразований адреса внутри процессора. Физический адрес:
- (для сегментной адресации) полностью совпадает с линейным адресом;
- (для сегментно-страничной адресации) получается путем преобразования трех частей значения линейного адреса на основании: каталога страниц, таблицы страниц и смещения внутри страницы;
Этот адрес выставляется на адресную шину процессора, может как совпадать с адресом ячейки оперативной памяти, так и не совпадать с ним;
Иными словами, существуют логическое , линейное и физическое адресные пространства, которые активно взаимодействуют между собой, на совокупности которых и строится любая операционная система. Фактически, процесс преобразования адресов, используемых программистом в своей программе в адрес ячейки оперативной памяти, является цепочкой преобразований от первого пункта к последнему и именуется трансляцией адресов («на лету», прозрачно преобразует один вид адреса в другой).
Механизм трансляции страниц (страничное преобразование)
Алгоритмы преобразования линейного адреса в физический (этапы 3 → 5) варьируются в зависимости от множества причин (состояния определенных регистров). В некоторых режимах линейный адрес делится на несколько частей, при этом каждая часть является индексом в специализированной системной таблице (все они расположены в памяти), а число и размер описанных таблиц различаются в зависимости от режима работы процессора. Запись в таблице первого уровня представляет собой адрес начала таблицы следующего уровня, а для последнего уровня — информация о физическом адресе страницы в памяти и её свойствах. Иначе говоря:
Соответственно, на данном этапе, мы уже имеем дело уже с разбиением адресного пространства на страницы (определенного размера) или со страничной организацией памяти. Т.е. мы имеем дело с виртуальной памятью. Процессор как бы делит линейное адресное пространство на блоки (страницы) фиксированного размера (в зависимости от установок — 4Кб, 2Мб, 4Мб), которые уже отображаются в физической памяти (или на диске). И вот тут стоит обратить внимание на один крайне важный аппаратный механизм:
Иными словами, преобразование линейного адреса в физический может закончиться неудачей, если:
- страницы в данный момент нет в физической памяти;
- таблицы не содержат необходимых данных;
- недостаточно прав доступа;
Во всех этих случаях возникает аппаратное событие — исключение Page Fault (#PF), которое предписывает обработчику исключения произвести дополнительные действия по устранению возникшей проблемы: подгрузить (отсутствующую) страницу с диска (либо скинуть (ненужную) страницу на диск). Как только страница была подгружена, то выполнение прерванного кода продолжится с инструкции, которая вызвала #PF. Именно механизм страничной адресации (преобразования) и позволяет операционной системе организовать виртуальное адресное пространство, о котором речь пойдет далее. К сожалению, подробное описание механизмов преобразования адресов и типов адресации выходит за рамки данной статьи, далее мы переходим к «программному» уровню, то есть непосредственно к механизмам операционной системы.
Адресация на уровне ОС
Сами понимаете, что было бы не совсем корректно называть излагаемое в данной главе некоей «программной» частью адресации в операционной системе, поскольку:
..поэтому операционная система Windows эксплуатирует особенности той архитектуры, на которой она в данный момент функционирует (выполняется) и всего-лишь использует аппаратные механизмы [процессора]. Становится очевидным, что если Windows исполняется на станциях, построенных на базе процессоров архитектуры IA-32 , то используется защищенный режим работы процессора. Версии операционной системы Windows для архитектуры IA-32 , пользуются механизмом сегментации защищенного режима лишь в минимальном объёме:
- используются всего два уровня привилегий: 0 и 3;
- и из всех доступных способов организации памяти используется защищенная плоская модель со страничной адресацией (protected flat model);
Есть соглашение, что все селекторы для процесса идентичны, это значит что адресация внутри процесса фактически базируется на смещении (а селекторы сегментов остаются неизменными). Исходя из этого, если каждый процесс [в системе] использует собственное адресное пространство линейным способом, то есть адресация базируется (фактически) на смещении, селекторы сегментов остаются неизменными, то сегментные регистры не нужны и их можно «опустить», «упразднить». На основе плоской модели памяти базируется часто упоминаемый в литературе механизм виртуальной памяти.
Возникает вопрос: почему это пространство называется виртуальным? А потому что виртуальный адрес может и не присутствовать в физической памяти , все механизмы [защищенного режима] созданы лишь для имитации (создания иллюзии для программы) его существования, ведь используются селекторы:смещения (которые могут ссылаться на любой адрес) совместно со страничным преобразованием (страницы могут быть сопоставлены с физической памятью, а могут и не быть), то есть все сущности по сути эфемерны, пользователь не знает как и где они размещены!!
Размерность адресных пространств
Не случайно во множественном числе, поскольку и эта тема несет в себе огромное количество неопределенностей и неточностей. Выше мы говорили о том, что существуют логическое , линейное и физическое адресные пространства в архитектуре процессора.
Физическое адресное пространство
Итак, размерность физического (процессорного) адресного пространства зависит от особенностей аппаратной архитектуры:
- 32-бита : используются 32-битные указатели (размерность 4 байта), и размер адресного пространства равен 2 32 = 4294967296 байт (4 гигабайта, Гб). Шестнадцатеричное представление диапазона: 00000000 — FFFFFFFF .
- 64-бита : используются 64-битные указатели (размерность 8 байт) и размер адресного пространства процесса равен 2 64 = 18446744073709551616 байт (16 экзабайт, Эб.
17 миллиардов гигабайт). Шестнадцатеричное представление диапазона: 0000000000000000 — FFFFFFFFFFFFFFFF .
Разрядность (битность) приложения | Разрядность указателя | Размер адресного пространства [процесса] | Адреса диапазона (шестнадцатеричные) |
---|---|---|---|
32 бита | 32 бита (4 байта) | 2 32 (4294967296 байт = 4 гигабайта) | 00000000 — FFFFFFFF |
64 бита | 64 бита (8 байт) | 2 64 (18446744073709551616 байт = |
17 миллиардов гигабайт = 16 экзабайт)
Но это, опять же, теоретическая адресация на основе разрядности.
Линейное адресное пространство
Линейное адресное пространство процесса теоретически могло бы быть идентично физическому адресному пространству, но на практике вступают в действие ограничения операционной системы , которые зависят от: версии операционной системы, [определенных] настроек (флагов) операционной системы и приложений, типа запуска: 32-битное приложение на 32-битной ОС, 32-битное приложение на 64-битной ОС, 64-битное на 64-битной ОС.
- 32-бита : в Windows размер линейного адресного пространства процесса равен 4Гб, верхние 2Гб (или 1Гб, в зависимости от флагов) из которых защищены на уровне страниц. Поэтому для пользовательского приложения в 32-битной ОС определен лимит в 2Гб (или 3, в зависимости от флагов), за пределы которого процесс выбраться не может (без использования специализированных технологий вида AWE).
- 64-бита : в Windows размер линейного адресного пространства процесса равен 16Тб или 256Тб, из которых (верхняя) часть защищена на уровне страниц. Поэтому пользовательскому приложению определен лимит в 8Тб и 128Тб соответственно.
Виртуальные адреса используются приложениями, однако сама операционная система (равно как и процессор) не способна по этим виртуальным адресам непосредственно обращаться к данным, потому как виртуальные адреса не являются адресами физического устройства хранения информации (ОЗУ/ДИСК), другими словами физически по этим адресам информация не хранится.
Или, если выразиться иначе, выполняемый код или используемые данные должны находиться в физической памяти, только в этом случае они будут выполнены/обработаны процессором.
Использование страничной организации операционной системой
Получается интересная ситуация: с одной стороны, для каждого процесса в операционной системе Windows выделяется адресное пространство, которое фактически эквивалентно размерности теоретического адресного пространства; с другой стороны размер физически установленной оперативной памяти (ОЗУ) компьютера может быть в разы меньше суммы всех адресных пространств процессов, исполняемых в данный момент в системе. Как нам в подобной ситуации обеспечить нормальное функционирование операционной системы? А очень просто, поскольку:
- виртуальные адреса суть иллюзия, они могут не ссылаться на физическую память;
- [в подавляющем большинстве случаев] процесс не использует всё виртуальное адресное пространство, отведенное для него; то есть адресное пространство процесса не обязательно заполнено [под завязку] данными;
- общие для всех процессов данные могут разделяться множеством процессов (экономия оперативной памяти);
- не обязательно код и данные всех процессов [постоянно] держать в ОЗУ (экономия оперативной памяти);
И в обеспечении всех этих механизмов нам на помощь приходит страничная организация (о которой говорилось выше): она позволяет операционной системе прозрачно (незаметно) для пользователя/приложения выполнять ряд очень нужных системе манипуляций:
- подгружать/выгружать неиспользуемые страницы с/на носитель информации (жесткий диск: HDD, SSD);
- проецировать общие страницы [общих ключевых библиотек] в несколько адресных пространств одновременно;
Страницы, которые в определенный промежуток времени не используются, из ОЗУ переносятся (перепроецируются) на любой физический носитель, установленный в системе — в файл (файл подкачки, страничный файл, page file, swap-файл, «своп») либо [в некоторых ОС] в область подкачки (специализированный раздел).
Сопоставлением (отображением) виртуальных адресов на физические адреса ОЗУ или файла подкачки занимается так называемый диспетчер виртуальной памяти (VMM, Virtual Memory Manager).
Упрощенная схема процесса «отображения» выглядит следующим образом:
Схема действительно упрощена, поскольку на деле в процессе сопоставления участвует множество структур: таблица указателей на каталог страниц, каталог страниц, таблица страниц. Как видно из нашего рисунка, виртуальные адреса могут проецироваться на физическую намять, файл подкачки или любой файл, располагающийся в файловой системе. На основе приведенной схемы мы может сделать довольно-таки важный вывод:
и теперь вы, надеюсь, понимаете, что:
Получается, что у виртуальных страниц адреса одни, аппаратные страницы выделяются по мере необходимости из имеющихся в наличии свободных страниц физической памяти, и, понятное дело, что физические адреса будут случайными и, в большинстве случаев, не будут совпадать с виртуальными. Напомню, что синхронизация между виртуальными и физическими страницами памяти обеспечивается аппаратно (на уровне процессора), называется страничным преобразованием и была нами описана выше. Наиболее значимые особенности виртуальной памяти таковы:
- Виртуальная память, доступная программе, напрямую не связана с физической памятью.
- Каждая программа работает в своей виртуальном адресном пространстве. Размер этого пространства может быть больше размера фактически установленной в машине оперативной памяти.
- Адресное пространство [каждого] процесса (программы), исполняющегося в ОС, изолировано [от подобных адресных пространств других процессов].
Когда какая-либо программа обращается к своим данным, которые [в этот момент] отсутствуют в ОЗУ, то функция обработчика страничного нарушения диспетчера виртуальной памяти производит следующие манипуляции:
- сохраняет в стек адрес инструкции, следующей за инструкцией, вызвавшей #PF;
- производит поиск свободной (незанятой) физической страницы;
- создает новый элемент в таблице страниц;
- подгружает недостающие данные из файла подкачки в ОЗУ;
- производит проецирование виртуальной страницы на физическую;
- производит восстановление адреса из стека и выполняет «перезапуск» инструкции (следующей за той, на которой было прервано выполнение);
Все эти процессы происходят на уровне ядра операционной системы, поэтому они «прозрачны» или «неразличимы» для пользовательского приложения (а программисту, в реалиях высокоуровневого программирования, зачастую и вовсе не интересны).
Теперь несколько слов об изоляции или закрытости [адресного пространства] процесса. Виртуальное пространство [каждого] процесса изолировано, или, можно сказать по-другому — процессы отделены друг от друга в своем собственном виртуальном адресном пространстве. Поэтому любой поток в рамках некоего процесса получает доступ только лишь к той памяти, которая принадлежит родительскому процессу. Наглядно, изолированность выражается в том, что некая программа A в своем адресном пространстве может хранить запись данных по условному адресу 12345678h , и в то же время у программы B по абсолютно тому же адресу 12345678h (но уже в его собственном адресном пространстве) могут находиться совершенно другие данные. Изолированность, к тому же подразумевает, что код одной программы (если быть точным, то потока в рамках процесса) не может получить доступ к памяти другой программы (без дополнительных манипуляций). Достоинства виртуальной памяти:
- Упрощается программирование. Программисту больше не нужно учитывать ограниченность памяти, или согласовывать использование памяти с другими приложениями.
- Повышается безопасность. Адресное пространство процесса изолировано.
- Однородность массива. Адресное пространство линейно.
Структура виртуального адресного пространства процесса
Я думаю, после некоторого количества теоретических выкладок, самое время перейти ближе к рассмотрению основной темы статьи. Напомню, что мы будем исследовать структуру памяти 32-битного процесса Windows. Для исследования памяти процесса нам потребуется специализированное программное средство, которое поможет нам увидеть адресное пространство процесса в деталях. Использовать мы будем утилиту VMMap от Марка Руссиновича, отличное приложение, которое выводит подробную информацию по использованию памяти в рамках того или иного процесса. Однако, не обошлось, что называется, и без ложки дегтя. Бытует мнение, что данное ПО отражает карту процесса не достаточно подробно, игнорируя кое-какие структуры памяти, однако, как отправная точка для понимания принципов размещения объектов в памяти вполне нас устроит.
Для практического эксперимента я буду использовать самописный модуль test2.exe , написанный на ассемблере, код которого предельно прост, отображает всего-лишь некоторые оконные элементы и выводит информационное окно перед выходом. В модуле используются (импортируются) функции SetFocus , SendMessageA , MessageBoxA , CreateWindowExA , DefWindowProcA , DispatchMessageA , ExitProcess , GetMessageA , GetModuleHandleA , LoadCursorA , LoadIconA , PostQuitMessage , RegisterClassA , ShowWindow , TranslateMessage , UpdateWindow из библиотек user32.dll и kernel32.dll . Итак, запускаем на исполнение тестовый файл test2.exe а затем, пока приложение исполняется, загружаем программу VMMap , указывая ей открыть наш целевой процесс. Вот что мы наблюдаем:
Поскольку информации довольно много, к карте процесса потребуется небольшое пояснение. И хотя это и не статья, описывающая функционал утилиты VMMap , однако мы должны осветить некоторые ключевые моменты, потому как без знаний о заголовках столбцов и видах типов регионов мы в изучении далеко не продвинемся.
Наименование столбца | Описание |
---|---|
Address | Стартовый адрес региона в виртуальном пространстве процесса. Шестнадцатеричное представление. |
Type | Тип региона (см. таблицу далее). |
Size | Полный размер выделенной области. Отражает максимальный размер физической памяти, которая необходима для хранения региона. Так же включает зарезервированные области. |
Committed | Количество памяти региона, которое «отдано», «передано» или «зафиксировано» — то есть эта память уже связана с ОЗУ, страничным файлом [подкачки], или с отображенным файлом (mapped file) [на диске]. |
Private | Часть всей памяти, выделенной для региона, которая приватна, то есть принадлежит исключительно процессу-владельцу и не может быть разделена с другими процессами. |
Total WS | Общее количество физической памяти, выделенной для региона (ОЗУ + файл подкачки). |
Private WS | Приватная часть физической памяти [региона/файла]. Принадлежит исключительно владельцу и не может быть разделена (использована совместно) с другими процессами. |
Shareable WS | Общедоступная часть физической памяти [региона/файла]. Может быть использована совместно другими процессами, которым так же необходим данный регион (файл). |
Shared WS | Общедоступная часть физической памяти [региона/файла]. Уже используется совместно с другими процессами. |
Locked WS | Часть физической памяти [региона/файла], которая гарантированно находится в ОЗУ и не вызывает ошибок страниц (необходимость подгрузки из файла подкачки), когда к ней пытаются получить доступ. |
Blocks | Количество выделенных в регионе блоков памяти. Блок — неразрывная группа страниц с идентичными атрибутами защиты, сопоставленная с одним регионом физической памяти. Если Вы посмотрите внимательно то заметите, что значения параметра «Blocks», отличные от нуля, встречаются в регионах, которые разбиты на несколько частей (подрегионы, блоки). Обычно имеется несколько подрегионов: основной регион + резервные. |
Protection | Типы операций, которые могут быть применены к региону. Для регионов, которые подразделяются на подблоки (+), колонка указывает общую (сводную) информацию по типам защиты в подблоках. В случае применения к региону неразрешенного типа операций, возникает «Ошибка доступа». Ошибка доступа происходит в случаях: когда происходит попытка запустить код из региона, который не помечен как исполняемый (если DEP включена), или при попытке записи в регион, который не помечен как предназначенный для записи или для «копирования-при-записи» (copy-on-write), или в случае попытки доступа к региону, который маркирован как «нет доступа» или просто зарезервирован, но не подтвержден. Атрибуты защиты присваиваются регионам виртуальной памяти на основе атрибутов сопоставленных регионов физической памяти. |
Details | Дополнительная информация по региону. Тут могут отображаться: путь файла бэкапа, идентификатор кучи (для региона heap), идентификатор потока (для стека), указатель на .NET-домен и прочее. |
Ну и необходимо описать все виды типов (type) регионов. Типы регионов у можно наблюдать на карте процесса в столбце Type :
Тип региона | Описание |
---|---|
Free | Диапазон виртуальных адресов не сопоставленных с физической памятью. Это память, которая еще не занята. Регион или часть региона доступны для резервирования (выделения). |
Shareable | Регион, который может быть разделен с другими процессами и забекаплен в физической памяти либо файле подкачки. Подобные регионы обычно содержат данные, которые разделены между процессами, то есть используются несколькими программами, через общие, специально оформленные, секции DLL или другие объекты. |
Private Data | Частные данные. Это регион, выделенный через функцию VirtualAlloc . Эта часть памяти не управляется менеджером кучи (Heap Manager), функциями .NET и не выделяется стеку. Обычно содержит данные приложения, которые используются только нашей программой и не доступны другим процессам. Так же содержит локальные структуры процесса/потока, такие как PEB или TEB . Типичная «память программы». Регион сопоставлен со страничным файлом. |
Unusable | Виртуальная память, которая не может быть использована из-за фрагментации. Это осколки, которые уже закреплены за регионом. Гранулярность выделения памяти в Windows — регионы по 64Кб. Когда Вы пытаетесь выделить память с помощью функции VirtualAlloc и запрашиваете, к примеру 8 килобайт, VirtualAlloc возвращает адрес региона в 64 килобайта. Оставшиеся 56Кб помечаются как неиспользуемые (unusable). Обратите внимание на то, что области Unusable «следуют» в карте за не кратными 64Кб регионами, на самом же деле, это всего-лишь память, которая входит в регион (принадлежит региону-владельцу), но на данный момент не используется. |
Image | Регион сопоставлен с образом исполняемого EXE- или DLL-файла, проецируемого в память. Это именно тот регион, куда загружается образ пользовательского приложения со всеми его секциями (в нашем случае test2.exe ). |
Image (ASLR) | Образы системных библиотек, загружаемые с использованием механизма безопасности ОС под названием ASLR (Address Space Layout Randomization). ASLR — рандомизация расположения в адресном пространстве процесса таких структур как: образ исполняемого файла, подгружаемая библиотека, куча и стек. Вкратце, ОС игнорирует предпочитаемый базовый адрес загрузки, который задан в заголовке PE и загружает библиотеку в адрес по выбору «менеджера загрузки». Для поддержки ASLR, библиотека должна быть скомпилирована со специализированной опцией, либо без неё, когда используется принудительная рандомизация (ForceASLR). Таким образом, усиливается безопасность процесса и исключаются конфликты базовых адресов образов [подгружаемых модулей]. Применяется начиная с Windows Vista. Технология так же известна под псевдонимом Rebasing. |
Thread Stack | Стек. Регион сопоставлен со стеком потока. Каждый поток имеет свой собственный стек, соответственно под каждый поток выделяется регион для хранения его собственного стека. Когда в процессе создается новый поток, система резервирует регион адресного пространства для стека потока. Для чего обычно используется стек? Ну как и все стеки, стек потока предназначается для хранения локальных переменных, содержимого регистров и адресов возврата из функций. |
Mapped File | Проецируемые файлы. Это немного не то же, что «проецирование» образа самой программы и необходимых библиотек. Все отображаемые в адресное пространство процесса файлы могут быть трех видов: самой программой, библиотеками, и рабочими объектами. Проецируемые (mapped) файлы это и есть вот эти самые рабочие объекты, которые может создавать и использовать код программы. Обычно это файлы, которые содержат какие-либо требующиеся приложению данные и с которыми приложение работает напрямую. Проецирование файлов — наиболее удобный способ обработки внешних данных, поскольку данные из файла становятся доступны непосредственно в адресном пространстве процесса (регион памяти сопоставлен с файлом или частью файла), а на самом деле они размещаются на диске. Таким образом программе файл доступен в виде большого массива, нет необходимости писать собственный код загрузки файла в память, на лицо экономия на операциях ввода-вывода и операциях с блоками памяти. ОС делает всё это прозрачно для разработчика, собственными механизмами, получается для кода область проецируемых файлов — это обычная память. Проецируемые файлы предназначены для операций с файлами из кода основной программы, ведь рано или поздно подобные операции с файлами приходится использовать практически во всех проектах, и зачастую это влечет за собой большое количество дополнительной работы, поскольку пользовательское приложение должно уметь работать с файлами: открывать, считывать и закрывать файлы, переписывать фрагменты файла в буфер и оттуда в другую область файла. В Windows все подобные проблемы решаются как раз при помощи проецируемых в память файлов (memory-mapped files). Проецируемый в память файл может иметь имя и быть разделяемым, то есть совместно использоваться несколькими приложениями. Работа с проецируемыми файлами в пользовательском режиме обеспечивается функциями CreateFileMapping и MapViewOfFile . |
Heap (Private Data) | Куча. Это регион зарезервированного адресного пространства процесса, предназначенный для динамического распределения небольших областей памяти. Представляет из себя закрытую область памяти, которая управляется так называемым «Менеджером кучи» (Heap Manager). Данные в этой области хранятся «в куче» (или «свалены в кучу»), то есть друг за другом, разнородные, без какой-либо систематизации. Смысл кучи сводится к обработке множества запросов на создание/уничтожение множества мелких объектов (блоков памяти). Куча используется различными Windows-функциями (API), вызываемыми кодом Вашего приложения, либо функциями самого приложения, для различных временных буферов хранения строк, переменных, структур, объектов. Память в куче выделяется участками (индексами), которые имеют фиксированный размер (8 байт). |
Как Вы видите из карты процесса, всё адресное пространство процесса разбито на множество неких зон различного назначения, называемых регионами. Регионов в адресном пространстве достаточно много. Однако, для начала, давайте посмотрим на «общее» разбиение адресного пространства процесса, дабы возникло понимание, как что и где может размещаться. Разбиение адресного пространства в определенной степени зависит от версии ядра Windows.
Общая концепция разбиения виртуального адресного пространства 32-битных программ:
Начало | Конец | Размер | Описание |
---|---|---|---|
00000000 | 0000FFFF | 64Кб | Область нулевых указателей . Зарезервировано. Данная область всегда маркируется как свободная (Free). Попытка доступа к памяти по этим адресам вызывает генерацию исключения нарушения доступа STATUS_ACCESS_VIOLATION . Область применяется для выявления программистами некорректных, нулевых указателей, тем самым позволяя выявлять некорректно работающий код. Если по каким-то причинам (напр.: возврат значения функцией) переменная или регистр вдруг принимает нулевое (неинициализированное) значение, то дальнейшая попытка обращения к памяти (запись/чтение) с использованием данной переменной/регистра приведет к генерации исключения (напр.: mov eax, dword ptr [esi], где ESI =0). |
00010000 | 7FFEFFFF | 2Гб (3Гб) | Пользовательский режим (User mode). Пользовательская часть кода и данных. В это пространство загружается пользовательское приложение, с разбивкой по секциям. Отображаются все проецируемые в память файлы, доступные данному процессу. В этом пространстве создаются пользовательская часть стеков потоков приложения. Тут присутствуют основные системные библиотеки ntdll.dll , kernel32.dll , user32.dll , gdi32.dll . |
7FFF0000 | 7FFFFFFF | 64Кб | Область некорректных указателей . Зарезервировано. Данная область всегда маркируется как свободная (Free). Попытка доступа к памяти по этим адресам вызывает генерацию исключения нарушения доступа STATUS_ACCESS_VIOLATION . Хотя эта область формально и относится к области памяти пользовательского режима, она является «пограничной», то есть имеется риск при операциях с большими блоками памяти выйти за границы пользовательского режима и перезаписать данные режима ядра, поэтому Microsoft предпочла заблокировать доступ к данной области. Область применяется для выявления некорректных (вышедших за пределы пользовательской памяти) указателей (переменные/регистры) в коде (например: mov eax, dword ptr [esi], где ESI =значение, входящее в диапазон 7FFF0000-7FFFFFFF). |
80000000 | FFFFFFFF | 2Гб (1Гб) | Режим ядра (Kernel mode). Код и данные модулей ядра, код драйверов устройств, код низкоуровневого управления потоками, памятью, файловой системой, сетевой подсистемой. Размещается кеш буферов ввода/вывода, области памяти, не сбрасываемые в файл подкачки. Таблицы, используемые для контроля страниц памяти процесса (PTE?). В этом пространстве создаются ядерная часть стеков для каждого потока в каждом процессе. Пространство недоступно из пользовательского режима, и попытка обращения из кода режима пользователя приведет к исключению нарушения доступа. Пространство «общее», то есть идентично (одинаково) для всех процессов системы. |
Ну, регионы мы бегло рассмотрели, давайте разберемся, как же происходит построение адресного пространства для конкретного процесса? Ведь должен существовать в системе механизм выделения и заполнения регионов. Все начинается с того, что пользователь либо некий код инициируют выполнение исполняемого модуля (программы). Одни из примеров подобного действия может быть двойной щелчок в проводнике по имени исполняемого файла. В этом случае код инициирующего выполнение потока вызывает функцию CreateProcess либо родственную из набора функций, предназначающихся для создания нового процесса. Какие же действия выполняет ядро после вызова данной функции:
- Находит исполняемый файл (.exe), указанный в параметре функции CreateProcess . В случае каких проблем просто возвращает управление со статусом false.
- Создает новый объект ядра «процесс».
- Создает адресное пространство процесса.
- Во вновь созданном адресном пространстве резервирует регион (набор страниц). Размер региона выбирается с расчетом, чтобы в него мог уместиться исполняемый .exe-файл. Загрузчик образа смотрит на параметр заголовка .exe-файла, который указывает желательное расположение (адрес) этого региона. По-умолчанию = 00400000 , однако может быть изменен при компиляции.
- Отмечает, что физическая память, связанная с зарезервированным регионом это сам .exe-файл на диске.
- После окончания процесс проекции .exe-файла на адресное пространство процесса, система анализирует секцию import directory table , в которой представлен список DLL-библиотек (которые содержат функции необходимые коду исполняемого файла) и список самих функций.
- Для каждой найденной DLL-библиотеки производится «отображение», то есть вызывается функция LoadLibrary , которая выполняет следующие действия:
- Резервирует регион в адресном пространстве процесса. Размер выбирается таковым, чтобы в регион мог поместиться загружаемый DLL-файл. Желаемый адрес загрузки DLL указывается в заголовке. Если размер региона по желаемому адрес меньше размера загружаемого DLL, либо регион занят, ядро пытается найти другой регион.
- Отмечает, что физическая память, связанная с зарезервированным регионом это DLL-файл на диске.
- Производится настройка образа библиотеки, сопоставление функций. Результатом этого является заполненная таблица (массив) адресов импортируемых функций, чтобы в процессе работы код обращается к своему массиву для определения точки входа в необходимую функцию.
Очевидно, что в момент создания процесса, адресное пространство практически все свободно (незарезервировано). Поэтому, для того, что бы воспользоваться какой-либо частью адресного пространства, надо эту самую часть для начала выделить (зарезервировать) посредством специализированных функций.
Резервирование региона происходит вместе с выравниванием начала региона с учетом так называемой гранулярности (зависит от процессора, 64Кб). При резервировании обеспечивается дополнительно еще и кратность размера региона размеру страницы (зависит от процессора, 4Кб), поэтому если вы пытаетесь зарезервировать регион некратной величины, система округлит значение до ближайшей большей кратной величины.
Размещение в адресном пространстве структур и библиотек
Далее по тексту я буду приводить описание непосредственно регионов памяти, делая цветовую маркировку для удобства сопоставления типа региона фактически размещаемым данным.
Адрес | Модуль | Описание |
---|---|---|
00040000 | apisetschema.dll | Предназначена для организации и разделения на уровни выполнения огромного количества функций базовых DLL системы. Подобная технология была названа «Наборами API» (API Sets), появилась в Windows 7 и предназначалась для группировки всех [многочисленных] функций в 34 различных типа и уровня выполнения, с целью предотвратить циклические зависимости между модулями и минимизировать проблемы с производительностью, которые обусловлены обеспечением зависимости новых DLL от набора Win32 API в адресном пространстве процесса. Перенаправляет вызовы, адресованные базовым DLL к их новым копиям, разделенным на уровни. |
00050000 | Стек потока | [64-битный] стек потока. |
00090000 | Стек потока. | Одномерный массив элементов с упорядоченными адресами (организованный по принципу «последний пришел — первым ушел» (LIFO)), предназначенный для хранения небольших объемов данных фиксированного размера (слово/двойное слово/четверное слово): стековых фреймов, передаваемых в функцию аргументов, локальных переменных функций, временно сохраняемых значений регистров. Для каждого потока выделяется собственный (отдельный) стек. Каждый раз при создании нового потока в контексте процесса, система резервирует регион адресного пространства для стека потока и передает данному региону определенный объем [физической] памяти. Для стека система резервирует 1024Кб (1Мб) адресного пространства и передает ему всего две страницы (2х8Кб?) памяти. Но перед фактическим выполнением потока система устанавливает указатель стека на конец верхней страницы региона стека, это именно та страница, с которой поток начнет использовать свой стек. Вторая страница сверху называется сторожевой (guard page). Как только активная страница «переполняется», поток вынужден обратиться к следующей (сторожевой) странице. В этом случае система уведомляется о данном факте и передает память еще одной странице, расположенной непосредственно за сторожевой. После чего флаг PAGE_GUARD переходит к странице, которой только что была передана память. Благодаря описанному механизму объем памяти стека увеличивается исключительно по мере необходимости. |
00280000 | msctf.dll.mui | Файл локализации библиотеки msctf.dll , описанной ниже. В общем смысле представляет собой переведенные на русский язык текстовые строки/константы, используемые библиотекой. |
00400000 | test2.exe | Собственно образ нашей программы. Отображается в виртуальном адресном пространстве благодаря системному механизму проецирования файлов. Исполняемый .exe-файл проецируется на адресное пространство программы по определенным адресам и становится его частью. Проецирование состоит в том, что данные [из файла] не копируются в память, а как бы связываются с данными на физическом носителе, то есть любое обращение к памяти по этим адресам инициирует чтение данных с диска, память как бы «читается» из файла на диске. Виртуальный адрес 00400000 является «предпочитаемой базой образа» (Image base), константой, которую можно изменять при компиляции. По традиции, никто этим не заморачивается, и, в большинстве случаев, данный адрес актуален для подавляющего большинства программ (но встречаются и исключения). |
Образ исполняемого файла (test2.exe) содержит в себе секции. Данный факт можно подтвердить, раскрыв (+) содержимое образа. Объясняется это тем, что exe-файл состоит из множества частей: непосредственно секция кода, секция данных, секция ресурсов, констант. Все эти секции загрузчик размещает по собственным областям памяти и назначает различные атрибуты доступа.
Представляет из себя программную среду, позволяющую исполнять 32-разрядные приложения на 64-разрядной версии Windows. Механизм используется в 64-разрядных версиях Windows в виде набора библиотек DLL пользовательского режима. Помимо данных библиотек в 64-разрядной версии ОС присутствует поддержка со стороны ядра (изменение контекста). Перехватывает системные вызовы 32-битных версий ntdll.dll и user32.dll , поступающих от 32-битных приложений и транслирует их в 64-битные вызовы ядра. С помощью Wow64 создаются 32-разрядные версии структур данных для процесса, например PEB , TEB и другие, на основе их 64-разрядных прототипов.
wow64win.dll | Библиотека, предназначенная для эмуляции системных вызовов графической оболочки пользователя (GUI), экспортируемых Win32k.sys. |
wow64.dll | Обеспечивает инфраструктуру эмуляции (преобразования) системных вызовов, экспортируемых ядром ntoskrnl.exe. По сути, организует перенаправление всего основного функционала, включающего операции с файловой системой и реестром. Управляет созданием процесса и потоков в нём. |
wow64cpu.dll | Библиотека, отвечающая за эмуляцию x86-инструкций на процессорах Itanium. Управляет 32-разрядным контекстом процессора для каждого потока, запущенного в рамках Wow64. Поддерживает переключение режима работы с 32-разрядного в 64-разрядный и наоборот, обеспечивая поддержку всей логики. Не обязательна для x64-процессоров, поскольку они выполняют x86-32-инструкции напрямую. |
Выводы
К каким выводам можно придти после изучения адресного пространства процесса? Первый состоит в том, что понятие «памяти» для пользовательских программ это достаточно условное обозначение, поскольку регионы адресного пространства могут по разному отображаться на различные объекты операционной системы. Второй состоит в том, что адресное пространство процесса это огромный линейный массив байтов, в котором хранится всё, с чем непосредственно работает процесс (программа). Массив этот виртуален, не ограничен физической памятью, уникален для каждого приложения и обладает достаточной размерностью, дабы программист не задумывался о его ограничениях. Механизм создания адресного пространства процесса достаточно сложен, и в статье удалось рассмотреть лишь малую часть его логики. В добавок, мы вовсе не касались 64-битных реалий, грозно смотрящих на нас из недалекого будущего 🙂 но, пожалуй это тема отдельной статьи.