Адресное пространство процесса в Windows

Метки:  , , , , , , , , ,

Я думаю, не будет слишком смелым заявление о том, что одним из основополагающих моментов в изучении работы операционной системы является понимание алгоритма управления оперативной памятью. Мало найдется желающих оспаривать утверждение, что оперативная память (ОЗУ) является тем компонентом персональных компьютеров, важность которого, при современной архитектуре вычислительных систем, сложно переоценить, и без которого работа их представляется невозможной. Мне вот всегда хотелось понять, как именно ОС управляет доступной ей памятью? Как она распределяет её между загруженными приложениями? Как она создает в памяти новый процесс, куда передает управление и как процессу выделяется дополнительная память по запросу, в случае, когда отведенной изначально памяти не хватает? Как организовано адресное пространство процесса? Подобные вопросы возникали и продолжают возникать у меня довольно часто, но я так же часто замечаю, что далеко не на все из них у меня находятся вразумительные ответы.
Поскольку круг вопросов, касающихся памяти настолько велик, что не может быть освещен в одной статье, здесь мы коснемся лишь части огромного механизма управления памяти в ОС Microsoft Windows, а именно изучим адресное пространство процесса, увидим, что же располагается в пространстве памяти, выделяемой процессу.

Память - физическое устройство или среда для хранения данных, используемая в вычислениях в течении определенного времени.

Память можно рассматривать как массив байт. Чтобы обратиться к конкретному байту (адресовать его), можно присвоить байту порядковый номер. Подобным образом можно поступить со всеми байтами, пронумеровав их целыми положительными числами, установив ноль за начало отсчёта. Индекс байта в этом огромном массиве и называется его адресом.

Давно, во времена, когда персональные компьютеры только-только обретали себя, память, доступная для программы, могла быть не более размера оперативной (физической) памяти машины, на которой она выполнялась. Это было просто, понятно и логично. Однако, подобная простота, помимо очевидных достоинств, накладывала и определенные неудобства. Одним из таких неудобств являлась проблема согласования выделения памяти различным приложениям. Размещение кода приложений в едином для всех программ пространстве памяти требовало сложного механизма постоянной коррекции адресов. Другой стороной вопроса был переход к многозадачным операционным системам, в которых большое количество задач выполнялось псевдопараллельно, что затрудняло использование общего пространства памяти. Еще одной проблемой была безопасность и необходимость ограничения доступа к "чужим" процессам в памяти. Эти и многие другие проблемы явились отправной точкой для работы над получением новых концепций организации памяти, которая завершилась открытием так называемой виртуальной памяти.

Виртуальная память – стратегия распределения памяти, основанная на идее расширения памяти путем создания единого адресного пространства, состоящего из физической памяти (ОЗУ) и памяти на носителе информации (жестком диске) и использования таблиц страниц (PTE) для трансляции адресов между ними.

Виртуальная память характеризуется своим собственным виртуальным адресным пространством, то есть адресацией, которая никак не связана с адресацией ОЗУ / файла подкачки. Создается отдельная логическая структура, некое адресное пространство, размерность которого зависит от разрядности операционной системы:

  • Для адресации виртуальной памяти 32-битного процесса в Windows используются 32-битные указатели (размерность 4 байта), и размер адресного пространства равен 232 = 4294967296 байт (4 гигабайта, Гб). Шестнадцатеричное представление диапазона: 00000000 - FFFFFFFF.
  • Для адресации виртуальной памяти 64-битного процесса в Windows используются 64-битные указатели (размерность 8 байт) и размер адресного пространства процесса равен 264 = 18446744073709551616 байт (16 экзабайт, Эб. ~17 миллиардов гигабайт). Шестнадцатеричное представление диапазона: 0000000000000000 - FFFFFFFFFFFFFFFF.
Разрядность процесса Разрядность указателя Размер адресного пространства Адреса диапазона
32 бита 32 бита (4 байта) 232 = 4294967296 байт (4 гигабайта, Гб) 00000000 - FFFFFFFF
64 бита 64 бита (8 байт) 264 = 18446744073709551616 байт (16 экзабайт, Эб. ~17 миллиардов гигабайт) 0000000000000000 - FFFFFFFFFFFFFFFF

Сказать иначе, у каждого процесса есть в распоряжении, в зависимости от разрядности, 4 Гб либо 16 Эб. На самом деле, это только в теории, а на практике есть определенные ограничения, например для 32-битного приложения имеется лимит в 2 гигабайта, за пределы которого процесс выбраться не может (без использования специализированных технологий на подобии AWE).
Виртуальные адреса используются при программировании, однако ОС не способна по этим адресам непосредственно обращаться к данным, потому как виртуальные адреса не являются адресами физического устройства хранения информации, другими словами физически по этим адресам ничего не хранится.

Для того, чтобы код, размещенный по виртуальным адресам можно было выполнить, такие адреса должны быть "отображены" на физические адреса, по которым действительно могут храниться код и данные. Так устроен центральный процессор. Или, если выразиться иначе, выполняемый код или используемые данные должны находиться в физической памяти, только в этом случае они будут выполнены/обработаны процессором.

То есть, код и данные, которые в данный момент обрабатываются/исполняются, физически располагаются в ОЗУ. Довольно важный момент для осознания того, как же работает ОС с памятью.
Сопоставлением (отображением) виртуальных адресов на физические адреса ОЗУ или файла подкачки занимается так называемый "диспетчер виртуальной памяти" (VMM, Virtual Memory Manager).
Вот как выглядит процесс "отображения" в общих чертах:
сопоставление виртуальных страниц процесса
Как видно из нашего рисунка, виртуальные адреса могут проецироваться на физическую намять, файл подкачки или любой файл, располагающийся в файловой системе.

Диспетчер виртуальной памяти - код уровня ядра, который служит для организации подсистемы виртуальной памяти, создания таблицы адресов для процессов, организации общего доступа к памяти, осуществления защиты на уровне страниц, поддержки возможность отображения файлов на память, распределения физической памяти между процессами, организации выгрузки/загрузки страниц между физической памятью и файлом подкачки, обеспечения всех процессов достаточным для функционирования объемом физической памяти.

На этом этапе возникает резонный вопрос, ведь у виртуальных страниц адреса одни, аппаратные страницы выделяются по мере необходимости из имеющихся в наличии свободных страниц физической памяти, и, понятное дело, что физические адреса будут случайными и, в большинстве случаев, не будут совпадать с виртуальными. Так как же, получается, происходит синхронизация между виртуальными и физическими страницами памяти, ведь код, который попадает на физическую страницу, должен как то выполняться исходя уже из физических адресов? Этот механизм поддерживается аппаратно на уровне процессора, и называется трансляцией адресов. Данная технология "на лету" преобразует виртуальный адрес в физический и обратно. Отсюда следует немаловажный вывод:

Некоторые механизмы, которые использует диспетчер виртуальной памяти в операционной системе, имеют аппаратную поддержку на уровне процессора.

Основные концепции виртуальной памяти таковы:

  • Память, доступная программе, никак не связана с физической памятью.
  • Каждая программа работает в своей виртуальном адресном пространстве. Размер этого пространства может быть больше размера установленной в машине оперативной памяти.
  • Адресное пространство процесса (программы) изолировано от других подобных адресных пространств.

Как уже было сказано, виртуальное адресное пространство процесса может быть больше размера установленной физической памяти, поскольку оно именно виртуальное и представляет собой всего лишь диапазон адресов. Как такое возможно и как операционная система обеспечивает всё это пространство физической памятью в случае необходимости? В этом случае применяется следующая стратегия. Если программы выделяют в их адресных пространствах больше памяти, чем есть в системе физической памяти, то данные, которые в данный момент времени не используются, из ОЗУ переносятся на любой физический носитель, установленный в системе. Традиционной для этой цели используется жесткий диск (hdd, ssd). Данные переносятся в так называемый файл подкачки (страничный файл, page file, swap-файл, "своп") либо на отдельный, специализированный раздел. Когда какая-либо программа обращается к своим данным, которые были выгружены на диск и отсутствуют в ОЗУ, то “менеджер виртуальной памяти” автоматически загружает недостающие данные из файла подкачки в оперативную памяти. Все эти процессы происходят на уровне ядра ОС, поэтому они "невидимы" или "незаметны" для пользовательского приложения.
Виртуальное пространство каждого процесса изолировано, или, можно сказать по-другому - процессы изолированы друг от друга в своем собственном виртуальном адресном пространстве. Поэтому любой поток некоего процесса получает доступ только лишь к той памяти, которая принадлежит его процессу. Наглядно, изолированность выражается в том, что некая программа A в своем адресном пространстве может хранить запись данных по адресу 12345678h, и в то же время у программы B по абсолютно тому же адресу 12345678h (но уже в его адресном пространстве) могут находиться совершенно другие данные. Изолированность, к тому же подразумевает, что код одной программы (если быть точным, то потока процесса) не может получить доступ к памяти другой программы.
Достоинства виртуальной памяти:

  • Упрощается программирование. Программисту больше не нужно учитывать ограниченность памяти, или согласовывать использование памяти с другими приложениями.
  • Повышается безопасность. Адресное пространство процесса изолировано.
  • Однородность массива. Адресное пространство линейно.

Структура виртуальной памяти процесса

Я думаю, после некоторого количества теоретических выкладок, самое время перейти уже непосредственно к практической стороне вопроса. Напомню, что мы будем исследовать структуру памяти 32-битного процесса Windows. Для исследования памяти процесса нам потребуется специализированное программное средство, которое поможет нам увидеть адресное пространство процесса в деталях. Использовать мы будем утилиту VMMap от Марка Руссиновича, отличное приложение, которое выводит подробную информацию по использованию памяти в рамках того или иного процесса. Однако, не обошлось, что называется, и без ложки дегтя. Бытует мнение, что данное ПО отражает карту процесса не достаточно подробно, игнорируя кое-какие структуры памяти, однако, как отправная точка для понимания принципов размещения объектов в памяти вполне нас устроит.
Для практического эксперимента я буду использовать свой собственный, самописный тестовый 32-битный файл 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, указывая ему требуемый целевой процесс. Вот что мы наблюдаем:

Карта памяти процесса Windows

Я тоже был удивлен обилием информации. Да есть на что посмотреть, и это всего-лишь небольшая тестовая утилита, которая, по сути, ничего особенного и не делает.
К карте процесса потребуется небольшое пояснение. И хотя это и не статья, описывающая функционал утилиты 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 домен и прочее.
WS (Working Set) - так называемый рабочий набор, то есть множество (массив) страниц физической памяти (ОЗУ), уже выделенных для процесса и использующихся для фактического хранения кода/данных. Когда требуется доступ к каким-либо адресам виртуальной памяти, фактически по этим адресам должна располагаться физическая память, потому что операции чтения/записи/выполнения могут производиться только с физической памятью. Поэтому, когда с данными адресами будет сопоставлена физическая память, она добавляется как раз к рабочему набору процесса (working set).

Ну и необходимо описать все виды типов (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). Область применяется для выявления некорректных, нулевых указателей в коде. Сюда попадают попытки обращения по не инициализированным переменным/регистрам. (mov eax, dword ptr [esi], где esi=0). Попытка чтения/записи по данным адресам вызовет генерацию исключения нарушения доступа и процесс завершится. Данная особенность позволяет программистам обнаруживать баги в коде.
00010000 7FFEFFFF 2Гб (3Гб) Пользовательский режим (User mode). Пользовательская часть кода и данных. В это пространство загружается пользовательское приложение, с разбивкой по секциям. Отображаются все проецируемые в память файлы, доступные данному процессу. В этом пространстве создаются пользовательская часть стеков потоков приложения. Тут присутствуют основные системные библиотеки ntdll.dll, kernel.dll, user32.dll, gdi32.dll.
7FFF0000 7FFFFFFF 64Кб Область некорректных указателей. Зарезервировано. Данная область всегда маркируется как свободная (Free). Область применяется для выявления некорректных, вышедших за пределы пользовательской памяти, указателей в коде. Сюда попадают попытки обращения по некорректно инициализированным переменным/регистрам. (mov eax, dword ptr [esi], где esi=значение, выходящее за пользовательскую область памяти). Попытка чтения/записи по данным адресам вызовет генерацию исключения нарушения доступа.
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, которая описывается ниже. В общем смысле представляет собой переведенные на русский язык текстовые константы, используемые библиотекой.
00400000 В памяти процесса по этому адресу располагается сама наша программа test2.exe. Наша программа отображается в виртуальном адресном пространстве благодаря механизму проецирования файлов. Exe-файл "test2.exe" проецируется на адресное пространство программы по определенным адресам и становится его частью. Проецирование означает, что данные не копируются, а именно как бы связываются с данными на физическом носителе, то есть любое обращение к памяти по этим адресам инициирует чтение данных с диска, то есть память "читается" из файла на диске. Виртуальный адрес 00400000 является "предпочитаемой базой образа" (Image base), константой, которую можно изменять при компиляции. По традиции, никто этим не заморачивается, и, в большинстве случаев, данный адрес актуален для подавляющего большинства программ. Однако, бывают и исключения.

Не путайте "базу образа" (image base) с "точкой входа" (entry point). Вторая представляет из себя адрес начала исполняемого кода. Обычно лежит по некоторому смещению относительно "базы образа".

Образ исполняемого файла (test2.exe) содержит в себе секции. Данный факт можно подтвердить, раскрыв (+) содержимое образа. Объясняется это тем, что exe-файл состоит из множества частей: непосредственно секция кода, секция данных, секция ресурсов, констант. Все эти секции загрузчик размещает по своим областям памяти и назначает различные атрибуты доступа.
00410000 locale.nls. NLS предоставляет поддержку местной раскладки клавиатуры и шрифтов. NLS позволяет приложениям устанавливать локаль для пользователя и получать (отображать) местные значения времени, даты, и других величин, отображаемых в формате региональных настроек.
01F80000 SoftDefault.nls.
02250000 StaticCache.dat.
735A0000 uxtheme.dll. Библиотека содержит нечто под названием "менеджер темы". Функционал позволяет менять визуальное представление интерфейса пользователя Windows без необходимости менять базовый (в ядре) функционал операционной системы.
73A30000 comctl.dll. Библиотека реализует готовые элементы управления, которые используются в графическом интерфейсе.
74B20000 C:\Windows\SysWOW64\dwmapi.dll. Интерфейс диспетчера окон рабочего стола (DWM, Desktop Windows Manager). DWM - графический интерфейс рабочего стола, использующийся в Windows Aero. Управляет объединением различных выполняющихся и визулизируемых окон с рабочим столом. В своей программе я никаких специфических функций Windows Aero не использую, но, могу предположить, что образ библиотеки dwmapi.dll отображается на адресное пространство процесса по причине включенного на уровне системы интерфейса Aero.
751F0000, 75250000, 752D0000 В адресном пространстве процесса по данным адресам находятся библиотеки DLL пользовательского режима, отвечающие за работу подсистемы Wow64. Они появились в нашем адресном пространстве не случайно, поскольку, напомню, что наш 32-разрядный процесс test2.exe запущен в 64-разрядной ОС Windows 7 Professional.

Wow64 (Windows 32-bit on Windows 64-bit) - эмуляция Win32 приложений на 64-разрядной ОС Windows.

Представляет из себя программную среду, позволяющую исполнять 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 Библиотека, отвечающая за работу с процессором. Управляет 32-разрядным контекстом процессора для каждого потока, запущенного в рамках Wow64. Поддерживает переключение режима работы с 32-разрядного в 64-разрядный и наоборот, обеспечивая поддержку всей логики.

75500000 C:\Windows\SysWOW64\msctf.dll. Библиотека расширяет функционал, предоставляемый службами Microsoft для работы с текстом (Microsoft Text Services). Среди функций библиотеки имеются функции усовершенствованной текстовой обработки и ввода текста. Функционал библиотеки msctf.dll предоставляет двунаправленный обмен между приложением и службами работы с текстом. Предоставляет поддержку различных языков.
75810000 C:\Windows\SysWOW64\msvcrt.dll. Системная библиотека Windows. Библиотека содержит в себе вспомогательные функций для работы с памятью, устройствами ввода/вывода, математическими функциями. Довольно много прототипов функций, используемых в языках C/C++ содержится в данной библиотеке.
758C0000 C:\Windows\SysWOW64\gdi32.dll. Одна из четырех основных библиотек поддержки Win32 API. Часть интерфейса графических устройств (GDI, Graphics Device Interface) или интерфейса между приложениями и графическими драйверами видеокарты, работающая в режиме пользователя. Содержит функции и методы для представления графических объектов и вывода их на устройство отображения, отвечает за отрисовку линий, кривых, обработку палитры и управление шрифтами, фактически полностью отвечает на графику. Приложения посылают запросы коду GDI, работающему в режиме пользователя, который пересылает их GDI режима ядра, а тот уже перенаправляет данные запросы драйверам графического адаптера. Моя программа не импортирует функции из gdi32.dll напрямую, однако могу предположить, что библиотека проецируется в адресное пространство моего процесса не случайно. Вероятно, в вызываемых из моей утилиты функциях есть ссылки на функции из gdi32.dll, либо эта библиотека в любом случае загружается в память каждого создаваемого процесса?
75A80000 C:\Windows\SysWOW64\advapi32.dll. Одна из четырех основных библиотек поддержки Win32 API. Содержит большое количество часто востребованных функций: работа с реестром, сервисами, выключение (перезагрузка) ПК, и прч. В системе присутствует огромное количество библиотек, которые статически слинкованы с библиотекой advapi32. Поэтому, без проецирования её в адресное пространство процесса никак не обойтись.
76С00000 C:\Windows\SysWOW64\kernel32.dll. Одна из четырех основных библиотек поддержки Win32 API. В библиотеке содержатся основные подпрограммы для поддержки работы подсистемы Win32. Много ключевых процедур и функций, которые используются в пользовательских программах, содержатся в библиотеке kernel32.dll. Это работа с процессами (GetModuleHandle, GetProcAddress, ExitProcess), вводом-выводом, памятью, синхронизацией. Ранее kernel32.dll загружался во всех контекстах процесса по одному и тому же адресу. Теперь, думаю именно из-за ASLR, в адресному пространстве каждого процесса он загружается по разным адресам? Большинство экспортируемых библиотекой kernel32.dll функций используют «родной» API напрямую.
77070000 C:\Windows\SysWOW64\user32.dll. Одна из четырех основных библиотек поддержки Win32 API. Эта библиотека проецируется практически в каждый процесс Win32. Библиотека содержит часто используемые функции для обработки сообщений, меню, взаимодействия. Напомню, что в моей программе используются такие функции как: SetFocus(), SendMessage(), MessageBox(), CreateWindowEx(), DefWindowProcA(), DispatchMessageA(), GetMessageA(), LoadCursorA(), LoadIconA(), PostQuitMessage(), RegisterClassA(), ShowWindow(), TranslateMessage(), UpdateWindow(). Все эти функции предоставляются системной библиотекой user32.dll, поэтому без проецирования её в адресное пространство моего процесса моя программа (test2.exe) работать не будет.
771F0000 C:\Windows\SysWOW64\KernelBase.dll. Результат технологии разделения на уровни выполнения базовых функций DLL. Содержит так называемые низкоуровневые функции, которые ранее помещались в библиотеках kernel32.dll и advapi32.dll. Теперь код направляет запросы к этой библиотеке низкоуровневых функций, вместо того, чтобы, как раньше, выполнять их напрямую.
77C00000 C:\Windows\System32\ntdll.dll. Библиотека, предоставляющая "родной" интерфейс (Native API) функций ядра. Все функции подсистемы Win32 можно разделить на две части: функции, требующие перехода в режим ядра и функции не требующие перехода в режим ядра. Для обработки API-функций пользовательского режима, которые требуют перехода в режим ядра и существует библиотека ntdll.dll. По своей структуре ntdll.dll представляет собой обычную библиотеку пользовательского режима. Однако, это своеобразный "мост" между функциями библиотек пользовательского режима и кодом, который реализует соответствующий функционал в ядре. Пользовательский режим (user mode) и режим ядра (kernel mode) существенно отличаются в реализации, однако пользовательский режим должен максимально сохранять совместимость с привычными (старыми) форматами входных/выходных данных функций, в то время как режим ядра может потребовать существенного видоизменения кода от версии Windows к версии. С этой точки зрения, ntdll.dll играет роль интерфейса совместимости между разными версиями ОС, и именно благодаря ей разработчики Microsoft могут свободно менять внутреннюю реализацию функций на стороне ядра, сохраняя, при этом, формат параметров функций пользовательского режима.
Обычно функции ntdll.dll не вызываются напрямую кодом пользовательского режима, он оперирует лишь функциями подсистемы Win32, а вот они то и содержат в своем коде вызовы к ntdll.dll. Можно сказать, что Native API создан с единственной целью - вызывать функции ядра, код которого располагается в нулевом кольце защиты. Большинство точек входа в Native API являются "заглушками", которые передают управление коду режима ядра.
77DE0000 C:\Windows\SysWOW64\ntdll.dll. То же самое, что и описанный ntdll.dll, только для 32-битного процесса.
7EFDB000 Thread Environment Block (TEB), Блок окружения потока. Структура данных, размещаемая в адресном пространстве процесса, которая содержит информацию о конкретном потоке в пределах основного (текущего) процесса (в нашем случае - test2.exe). Каждый поток имеет свой TEB. Заполняется через функцию MmCreateTeb и заполняется загрузчиком потока. Создается, контролируется и разрушается исключительно самой ОС. Подобные регионы создаются и уничтожаются по мере появления/уничтожения потоков в процессе. Wow64 процессы имеют два TEB для каждого потока?
7EFDE000 Process Environment Block (PEB), Блок переменных окружения процесса. Структура данных, размещенная в адресном пространстве процесса, которая содержит информацию о загруженных модулях (LDR_DATA), окружении, базовой информации и другие данные, которые требуются для нормального функционирования процесса. Создается через функцию MmCreatePeb и заполняется загрузчиком процесса на этапе создания адресного пространства процесса. PEB создается, контролируется и уничтожается исключительно самой ОС. Wow64 процессы имеют два PEB для каждого процесса?
80000000 Память выше данного значения принадлежит ядру. В этой области памяти находятся модули ядра, объекты ядра и пользовательские объекты, доступные всем процессам - проекции системных файлов. Но это все уже тема отдельной статьи.

Выводы

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

  • Поделиться:

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *