Что такое страница памяти какие страницы используются в windows

Трансляция адресов

Трансляция виртуального адреса – это определение реального (физического) расположение ячейки памяти с данным виртуальным адресом, т. е. преобразование виртуального адреса в физический. Принцип трансляции показан на рис.11.1, здесь мы рассмотрим подробности трансляции и детали реализации в WRK.

Из рис.11.1 видно, что информация о соответствии виртуальных адресов физическим хранится в таблицах страниц. В системе для каждого процесса поддерживается множество записей о страницах: если размер страницы 4 КБ, то чтобы хранить информацию обо всех виртуальных страницах в 32 разрядной системе требуется более миллиона записей (4 ГБ / 4 КБ = 1 048 576). Эти записи о страницах сгруппированы в таблицы страниц (Page Table), запись называется PTE (Page Table Entry). В каждой таблице содержится 1024 записи, таким образом, максимальное количество таблиц страниц для процесса – 1024 (1 048 576 / 1024 = 1024). Половина от общего количества – 512 таблиц – отвечают за пользовательское ВАП, другая половина – за системное ВАП.

Таблицы страниц хранятся в виртуальной памяти (см. рис.11.2). Информация о расположении каждой из таблиц страниц находится в каталоге страниц (Page Directory), единственном для процесса. Записи этого каталога называются PDE (Page Directory Entry). Таким образом, процесс трансляции является двухступенчатым: сначала по виртуальному адресу определяется запись PDE в каталоге страниц, затем по этой записи находится соответствующая таблица страниц, запись PTE которой указывает на требуемую страницу в физической памяти.

Откуда процесс знает, где в памяти хранится каталог страниц? За это отвечает поле DirectoryTableBase структуры KPROCESS (файл base\ntos\inc\ke.h, строка 958, первый элемент массива).
Схема трансляции адресов показана на рис.11.3.

Трансляция адресов

Рис.
11.3.
Трансляция адресов

Записи PDE и PTE представлены структурой MMPTE_HARDWARE (base\ntos\mm\i386\mi386.h, строка 2508), содержащей следующие основные поля:

  • флаг (однобитовое поле) Valid: если виртуальная страница расположена в физической памяти, Valid = 1;
  • флаг Accessed: если к странице были обращения для чтения, Accessed = 1;
  • флаг Dirty: если содержимое страницы было изменено (была произведена операция записи), Dirty = 1;
  • флаг LargePage: если страница является большой (4 МБ), LargePage = 1;
  • флаг Owner: если страница доступна из пользовательского режима, Owner = 1;
  • 20 битовое поле PageFrameNumber: указывает номер страничного фрейма (PFN, Page Frame Number).

В поле PageFrameNumber хранится номер записи в базе данных PFN – системной структуре, отвечающей за информацию о страницах физической памяти. Запись PFN представлена структурой MMPFN (файл base\ntos\mm\mi.h, строка 1710) и подробно описана в [5, стр. 502].

Ошибки страниц

Страница может находиться либо в физической памяти (ОЗУ), либо на диске в файле подкачки.

Если в записи PTE флаг Valid = 1, то страница находится в физической памяти и к ней можно обращаться. Иначе (Valid = 0) – страница недоступна процессу. При попытке доступа к такой странице возникает страничная ошибка (page fault) и вызывается функция MmAccessFault (файл base\ntos\mm\mmfault.c, строка 101).

Причин страничных ошибок существует множество (см. [Руссинович и др., 2008, стр. 463]), мы рассмотрим только одну – страница выгружена в страничный файл (файл подкачки). В этом случае запись PTE имеет тип MMPTE_SOFTWARE (файл base\ntos\mm\i386\mi386.h, строка 2446) и вместо поля PageFrameNumber имеет 20 разрядное поле PageFileHigh, отвечающее за расположение страницы в страничном файле.

Страничные файлы описываются структурой MMPAGING_FILE (base\ntos\mm\mi.h, строка 4239), имеющей следующие поля:

  • Size – текущий размер файла (в страницах);
  • MaximumSize, MinimumSize – максимальный и минимальный размеры файла (в страницах);
  • FreeSpace, CurrentUsage – число свободных и занятых страниц;
  • PageFileName – имя файла;
  • PageFileNumber – номер файла;
  • FileHandle – дескриптор файла.

В 32 разрядных Windows поддерживается до 16 файлов подкачки размером до 4095 МБ каждый. Список файлов подкачки находится в ключе реестра HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\PagingFiles. Соответствующий системный массив MmPagingFile[MAX_PAGE_FILES] типа PMMPAGING_FILE описывается в файле base\ntos\mm\mi.h (строка 8045).

Пределы памяти

В таблицах 8.1, 8.2 и 8.3 приведены ограничения на виртуальную и физическую память в 32 разрядных и 64 разрядных операционных системах Windows.

Таблица
11.1.
Ограничения на виртуальную память

Тип памяти 32-разрядные Windows 64-разрядные Windows
Виртуальное адресное пространство 4 ГБ 16 ТБ (16 000 ГБ)
Пользовательское ВАП 2 ГБ; до 3 ГБ в случае использования специальных ключей при загрузке 8 ТБ
Системное ВАП 2 ГБ; от 1 до 2 ГБ в случае использования специальных ключей при загрузке 8 ТБ

Таблица
11.2.
Ограничения на физическую память в клиентских версиях

Версия Windows 32-разрядные 64-разрядные
Windows XP От 512 МБ (Starter)
до 4 ГБ (Professional)
128 ГБ (Professional)
Windows Vista от 1 ГБ (Starter)
до 4 ГБ (Ultimate)
от 8 ГБ (Home Basic)
до 128 ГБ (Ultimate)
Windows 7 от 2 ГБ (Starter)
до 4 ГБ (Ultimate)
от 8 ГБ (Home Basic)
до 192 ГБ (Ultimate)

Таблица
11.3.
Ограничения на физическую память в серверных версиях

Версия Windows 32-разрядные 64-разрядные
Windows Server 2003 R2 От 4 ГБ (Standard)
до 64 ГБ (Datacenter)
От 32 ГБ (Standard)
до 1 ТБ (Datacenter)
Windows Server 2008 От 4 ГБ (Web Server)
до 64 ГБ (Datacenter)
От 32 ГБ (Web Server)
до 1 ТБ (Datacenter)
Windows Server 2008 R2 нет 32 разрядных версий от 8 ГБ (Foundation)
до 2 ТБ (Datacenter)

Резюме

В лекции изучаются такие важные понятия как виртуальная и физическая память, виртуальное адресное пространство, рабочий набор процесса, файл подкачки. Рассматриваются структура виртуального адресного пространства процесса, способы выделения памяти процессам, дескрипторы виртуальных адресов, ошибки страниц. Описывается процесс трансляции виртуальных адресов в физические. Приводятся ограничения на размер виртуальной и физической памяти в различных версиях Windows.

Следующая лекция посвящена принципам обеспечения безопасности в Windows.

Контрольные вопросы

  • Дайте определение «виртуальной памяти». Чем виртуальная память отличается от физической?
  • Нарисуйте структуру виртуального адресного пространства в 32 разрядной системе Windows.
  • Что такое страница памяти? Какие страницы используются в Windows?
  • Опишите достоинства и недостатки различных способов выделения памяти.
  • Опишите процесс трансляции адресов. Какую роль в этом процессе играют таблицы страниц? Каталоги страниц?
  • Возможно ли в 32 разрядной системе Windows наличие большего объема физической памяти, чем виртуальной памяти процесса?

Уровень сложностиПростой

Время на прочтение15 мин

Количество просмотров13K

В этом посте я буду говорить о страничной организации только в контексте PML4 (Page Map Level 4), потому что на данный момент это доминирующая схема страничной организации x86_64 и, вероятно, останется таковой какое-то время.

Окружение

Это необязательно, но я рекомендую подготовить систему для отладки ядра Linux с QEMU + gdb. Если вы никогда этого не делали, то попробуйте такой репозиторий: easylkb (сам я им никогда не пользовался, но слышал о нём много хорошего), а если не хотите настраивать окружение самостоятельно, то подойдёт режим практики в любом из заданий по Kernel Security на pwn.college (вам нужно знать команды vm connect и vm debug).

Я рекомендую вам так поступить, потому что считаю, что самостоятельное выполнение команд вместе со мной и возможность просмотра страниц (page walk) на основании увиденного в gdb — хорошая проверка понимания.

Что такое страница

В x86_64 страница — это срез памяти размером 0x1000 байтов, выровненный по 0x1000 байтам.

Именно поэтому при изучении /proc/<pid>/maps видно, что все диапазоны адресов начинаются и заканчиваются адресами, заканчивающимися на 0x000, ведь минимальный размер распределения памяти в x86_64 равен размеру страницы (0x1000 байтов), а страницы должны быть «выровнены по страницам» (последние 12 битов должны быть равны нулю).

При помощи MMU виртуальную страницу (Virtual Page) можно резолвить в единую физическую страницу (Physical Page) (она же «блок страницы», Page Frame), хотя и многие виртуальные страницы могут ссылаться на одну физическую.

Что такое виртуальный адрес

Как можно догадаться, PML4 имеет четыре уровня структур страничной организации памяти; эти структуры называются таблицами страниц (Page Table). Таблица страниц — это область памяти размером со страницу, содержащая 512 8-байтных элементов таблицы страниц. Каждый элемент таблицы страниц ссылается или на таблицу страниц следующего уровня или на конечный физический адрес, в который резолвится виртуальный адрес.

Элемент таблицы страниц, используемый для трансляции адресов, основан на виртуальном адресе доступа к памяти. Так как на каждый уровень используется 512 элементов, 9 битов виртуального адреса применяются на каждом уровне для индексации в соответствующей таблице страниц.

Допустим, у нас есть такой адрес:

0x7ffe1c9c9000

Последние 12 битов адреса обозначают смещение внутри физической страницы:

0x7ffe1c9c9000 & 0xfff = 0x0

Это значит, что определив физический адрес страницы, в которую резолвится этот виртуальный адрес, мы добавим ноль к результату, чтобы получить конечный физический адрес.

После последних 12 битов, которые являются смещением внутри конечной страницы, виртуальный адрес состоит из индексов таблиц страниц. Как говорилось выше, каждый уровень страничной организации памяти использует 9 битов виртуального адреса, поэтому самый нижний уровень структур страничной организации, то есть таблица страниц, индексируется по следующим 9 битам адреса (благодаря битовому маскированию при помощи & 0x1ff для сдвинутого значения). На следующих уровнях нам просто нужно каждый раз выполнять сдвиг вправо ещё на девять битов и снова маскировать нижние девять битов в качестве нашего индекса. Выполнение этой операции для показанного выше адреса даёт нам следующие индексы:

Level 1, Page Table (PT):
Index = (0x7ffe1c9c9000 >> 12) & 0x1ff = 0x1c9

Level 2, Page Middle Directory (PMD):
Index = (0x7ffe1c9c9000 >> 21) & 0x1ff = 0x0e4

Level 3, Page Upper Directory (PUD):
Index = (0x7ffe1c9c9000 >> 30) & 0x1ff = 0x1f8

Level 4, Page Global Directory (PGD):
Index = (0x7ffe1c9c9000 >> 39) & 0x1ff = 0x0ff

База

Разобравшись, как выполнять индексацию в таблицах страниц, и поняв, что они приблизительно содержат, нужно узнать, где они находятся конкретно.

У каждого потока CPU есть базовый регистр таблицы страниц под названием cr3.

cr3 содержит физический адрес самого верхнего уровня структуры страничной организации, называемого Page Global Directory (PGD).

При отладке ядра через gdb содержимое cr3 можно считать следующим образом:

gef➤  p/x $cr3
$1 = 0x10d664000

В зависимости от используемых функций процессора в регистре cr3 наряду с адресом PGD может храниться и дополнительная информация, поэтому более универсальный способ получения физического адреса PGD из регистра cr3 заключается в маскировании нижних 12 битов его содержимого:

gef➤  p/x $cr3 & ~0xfff
$2 = 0x10d664000

Элементы таблиц страниц

Давайте рассмотрим в gdb физический адрес, полученный нами из cr3. Команда monitor xp/... раскрытая gdb благодаря QEMU Monitor, позволяет нам выводить физическую память vm, а команда monitor xp/512gx ... печатает всё содержимое (512 элементов) PGD, на который ссылается cr3:

gef➤  monitor xp/512gx 0x10d664000
...
000000010d664f50: 0x0000000123fca067 0x0000000123fc9067
000000010d664f60: 0x0000000123fc8067 0x0000000123fc7067
000000010d664f70: 0x0000000123fc6067 0x0000000123fc5067
000000010d664f80: 0x0000000123fc4067 0x0000000123fc3067
000000010d664f90: 0x0000000123fc2067 0x000000000b550067
000000010d664fa0: 0x000000000b550067 0x000000000b550067
000000010d664fb0: 0x000000000b550067 0x0000000123fc1067
000000010d664fc0: 0x0000000000000000 0x0000000000000000
000000010d664fd0: 0x0000000000000000 0x0000000000000000
000000010d664fe0: 0x0000000123eab067 0x0000000000000000
000000010d664ff0: 0x000000000b54c067 0x0000000008c33067

Вывод получается большим и почти весь он состоит из нулей, поэтому я привожу здесь только самый конец.

Вероятно, этот вывод пока для вас не имеет особого смысла, но мы можем заметить в данных определённые паттерны, например, многие 8-байтные элементы заканчиваются на 0x67.

Расшифровка записей PGD

Возьмём из показанного выше вывода PGD в качестве примера запись PGD по адресу 0x000000010d664f50 со значением 0x0000000123fca067, чтобы понять, как расшифровать запись.

И давайте сделаем это с двоичной формой значения этой записи:

gef➤  p/t 0x0000000123fca067
$6 = 100100011111111001010000001100111

Вот небольшая схема с объяснениями, что означает каждый бит записи:

~ PGD Entry ~                                                   Present ──────┐
                                                            Read/Write ──────┐|
                                                      User/Supervisor ──────┐||
                                                  Page Write Through ──────┐|||
                                               Page Cache Disabled ──────┐ ||||
                                                         Accessed ──────┐| ||||
                                                         Ignored ──────┐|| ||||
                                                       Reserved ──────┐||| ||||
┌─ NX          ┌─ Reserved                             Ignored ──┬──┐ |||| ||||
|┌───────────┐ |┌──────────────────────────────────────────────┐ |  | |||| ||||
||  Ignored  | ||               PUD Physical Address           | |  | |||| ||||
||           | ||                                              | |  | |||| ||||
0000 0000 0000 0000 0000 0000 0000 0001 0010 0011 1111 1100 1010 0000 0110 0111
       56        48        40        32        24        16         8         0

Вот что означает каждая из этих меток:

  • NX (неисполняемый) — если этот бит установлен, никакое из отображений памяти, являющихся потомком этого PGD, не будет исполняемым.

  • Reserved — эти значения должны быть равны нулю.

  • PUD Physical Address — физический адрес PUD, связанного с этой записью PGD.

  • Accessed — если эта запись или её потомки ссылаются на какую‑то страницу, то этот бит устанавливается MMU и может быть сброшен операционной системой.

  • Page Cache Disabled (PCD) — страницы‑потомки этой записи PGD не должны попадать в иерархию кэшей CPU; иногда этот бит также называют Uncacheable (UC).

  • Page Write Through (WT) — записи в страницы‑потомки этой записи PGD должны сразу же выполнять запись в ОЗУ, а не буферизировать записи в кэш CPU перед обновлением ОЗУ.

  • User/Supervisor — если этот бит сброшен, к страницам‑потомкам этой PGD невозможно выполнить доступ ни из какого режима, за исключением supervisor.

  • Read/Write — если этот бит сброшен, в страницы‑потомки этой PGD нельзя выполнять запись.

  • Present — если этот бит сброшен, то процессор не будет использовать эту запись для трансляции адресов и ни один из остальных битов не будет применяться.

Здесь нас волнует бит Present, биты, определяющие физический адрес следующего уровня структур страничной организации, биты PUD Physical Address и биты разрешений: NX, User/Supervisor, and Read/Write.

  • Бит Present очень важен, потому что без него вся остальная часть записи игнорируется.

  • PUD Physical Address позволяет нам продолжить просмотр страниц (page walk), сообщая, где находится физический адрес следующего уровня структур страничной организации памяти.

  • Биты Permission применяются к страницам, являющимся наследниками записи PGD; они определяют, как можно выполнять доступ к этим страницам.

Остальные биты для наших целей не так важны:

  • Бит Accessed устанавливается, если запись используется при трансляции доступа к памяти, он не важен для просмотра страниц.

  • Page Cache Disabled и Page Write Through не используются для обычного отображения страниц и не влияют на трансляцию страниц и разрешения, так что не будем обращать на них внимания.

Итак, декодировав эту запись, мы получим:

PUD является Present:

gef➤  p/x 0x0000000123fca067 & 0b0001
$18 = 0x1

Отображения в PUD и ниже могут быть Writable:

gef➤  p/x 0x0000000123fca067 & 0b0010
$19 = 0x2

Отображения в PUD и ниже могут быть доступны для User:

gef➤  p/x 0x0000000123fca067 & 0b0100
$20 = 0x4

Физический адрес PUD (биты (51:12] ) — 0x123fca000:

gef➤  p/x 0x0000000123fca067 & ~((1ull<<12)-1) & ((1ull<<51)-1)
$21 = 0x123fca000

Отображения в PUD и ниже могут быть Executable:

gef➤  p/x 0x0000000123fca067 & (1ull<<63)
$22 = 0x0

Декодирование записей для всех уровней

Разобравшись, как декодировать запись PGD, нам будет легко декодировать остальные уровни, по крайней мере, в общем случае.

На всех этих диаграммах X означает, что бит может быть и нулём, и единицей; в противном случае, если биту присвоено конкретное значение, то оно требуется или для архитектуры или для конкретной кодировки, показанной на диаграмме.

PGD

~ PGD Entry ~                                                   Present ──────┐
                                                            Read/Write ──────┐|
                                                      User/Supervisor ──────┐||
                                                  Page Write Through ──────┐|||
                                               Page Cache Disabled ──────┐ ||||
                                                         Accessed ──────┐| ||||
                                                         Ignored ──────┐|| ||||
                                                       Reserved ──────┐||| ||||
┌─ NX          ┌─ Reserved                             Ignored ──┬──┐ |||| ||||
|┌───────────┐ |┌──────────────────────────────────────────────┐ |  | |||| ||||
||  Ignored  | ||               PUD Physical Address           | |  | |||| ||||
||           | ||                                              | |  | |||| ||||
XXXX XXXX XXXX 0XXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX 0XXX XXXX
       56        48        40        32        24        16         8         0

Эту диаграмму мы уже видели, я подробно описал её в предыдущем разделе, но здесь у неё не указана конкретная запись PGD.

PUD

~ PUD Entry, Page Size unset ~                                  Present ──────┐
                                                            Read/Write ──────┐|
                                                      User/Supervisor ──────┐||
                                                  Page Write Through ──────┐|||
                                               Page Cache Disabled ──────┐ ||||
                                                         Accessed ──────┐| ||||
                                                         Ignored ──────┐|| ||||
                                                      Page Size ──────┐||| ||||
┌─ NX          ┌─ Reserved                             Ignored ──┬──┐ |||| ||||
|┌───────────┐ |┌──────────────────────────────────────────────┐ |  | |||| ||||
||  Ignored  | ||               PMD Physical Address           | |  | |||| ||||
||           | ||                                              | |  | |||| ||||
XXXX XXXX XXXX 0XXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX 0XXX XXXX
       56        48        40        32        24        16         8         0

Как видите, показанная выше диаграмма PUD очень похожа на диаграмму PGD, единственное различие заключается в появлении бита Page Size. Установленный бит Page Size сильно меняет интерпретацию записи PUD. В этой диаграмме мы предполагаем, что он сброшен, как и бывает чаще всего.

PMD

~ PMD Entry, Page Size unset ~                                  Present ──────┐
                                                            Read/Write ──────┐|
                                                      User/Supervisor ──────┐||
                                                  Page Write Through ──────┐|||
                                               Page Cache Disabled ──────┐ ||||
                                                         Accessed ──────┐| ||||
                                                         Ignored ──────┐|| ||||
                                                      Page Size ──────┐||| ||||
┌─ NX          ┌─ Reserved                             Ignored ──┬──┐ |||| ||||
|┌───────────┐ |┌──────────────────────────────────────────────┐ |  | |||| ||||
||  Ignored  | ||                PT Physical Address           | |  | |||| ||||
||           | ||                                              | |  | |||| ||||
XXXX XXXX XXXX 0XXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX 0XXX XXXX
       56        48        40        32        24        16         8         0

Диаграмма PMD тоже очень похожа на предыдущую, и, как и в случае с записью PUD, мы игнорируем бит Page Size.

PT

~ PT Entry ~                                                    Present ──────┐
                                                            Read/Write ──────┐|
                                                      User/Supervisor ──────┐||
                                                  Page Write Through ──────┐|||
                                               Page Cache Disabled ──────┐ ||||
                                                         Accessed ──────┐| ||||
┌─── NX                                                    Dirty ──────┐|| ||||
|┌───┬─ Memory Protection Key              Page Attribute Table ──────┐||| ||||
||   |┌──────┬─── Ignored                               Global ─────┐ |||| ||||
||   ||      | ┌─── Reserved                          Ignored ───┬─┐| |||| ||||
||   ||      | |┌──────────────────────────────────────────────┐ | || |||| ||||
||   ||      | ||            4KB Page Physical Address         | | || |||| ||||
||   ||      | ||                                              | | || |||| ||||
XXXX XXXX XXXX 0XXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX
       56        48        40        32        24        16         8         0

В записи Page Table всё становится интереснее: мы видим несколько новых полей/атрибутов, которых не было на предыдущих уровнях.

Вот какие это поля/атрибуты:

  • Memory Protection Key (MPK или PK): это расширение x86_64, позволяющее назначать страницам 4-битные ключи, что можно использовать, чтобы конфигурировать разрешения памяти для всех страниц с этим ключом.

  • Global: этот атрибут связан с тем, как TLB (Translation Lookaside Buffer — кэш MMU для трансляции виртуальных адресов в физические) кэширует трансляцию для страницы; если этот бит установлен, то страница не будет удалена из TLB при переключении контекста; обычно он включён для страниц ядра, чтобы снизить количество промахов TLB.

  • Page Attribute Table (PAT): если значение установлено, то MMU должно обратиться к Page Attribute Table MSR, чтобы определить Memory Type страницы, например, является ли эта страница Uncacheable, Write Through или имеет один из нескольких других типов памяти.

  • Dirty: этот бит похож на бит Accessed, он устанавливается MMU, если в эту страницу выполнена запись, и должен быть сброшен операционной системой.

Ничто из этого не влияет на саму трансляцию адреса, однако конфигурация Memory Protection Key может означать, что ожидаемые разрешения доступа к памяти для страницы, на которую ссылается эта запись, могут быть строже, чем закодированные в самой записи.

В отличие от предыдущих уровней, поскольку это последний, запись хранит последний физический адрес страницы, связанной с виртуальным адресом, который мы транслируем. Применив битовую маску для получения байтов физического адреса и добавив последние 12 байтов исходного виртуального адреса (смещение внутри страницы), мы получим физический адрес!

В общем случае просмотр страниц выполняется всего за несколько этапов:

  • Преобразуем виртуальный адрес в индексы и смещение страницы, сдвинув адрес и применив битовые маски

  • Считываем cr3, чтобы получить физический адрес PGD

  • Чтобы достичь каждого уровня до последнего:

    • Используем индексы, вычисленные из виртуального адреса, чтобы узнать, какую запись из таблицы страниц использовать

    • Применяем битовую маску к записи, чтобы получить физический адрес следующего уровня

  • На последнем уровне снова находим запись, соответствующую индексу из виртуального адреса

  • Применяем битовую маску, чтобы получить физический адрес страницы, связанный с виртуальным адресом

  • Добавляем смещение внутри страницы от виртуального адреса до физического адреса страницы

  • Готово!

Увеличиваем масштаб

Как говорилось выше, диаграммы PUD и PMD рассчитаны на общий случай, когда не установлен бит Page Size.

А что происходит, если он установлен?

Если он установлен, то он, по сути, говорит MMU: сворачивайся, мы здесь закончили, не храни просмотр страниц, текущая запись содержит физический адрес страницы, которую мы ищем.

Но это ещё не всё: физический адрес в записях, где установлен бит Page Size, предназначен не для обычной страницы на 4 КБ (0x1000 байтов), это Huge Page, имеющая два варианта: Huge Page на 1 ГБ и Huge Page на 2 МБ.

Когда бит Page Size установлен у записи PUD, то он ссылается на 1-гигабайтную Huge Page, а когда бит Page Size установлен у записи PMD, он ссылается на 2-мегабайтную Huge Page.

Но откуда берутся числа 1 ГБ и 2 МБ?

Каждый уровень таблиц страниц хранит до 512 записей, то есть PT может ссылаться на не более чем 512 страниц и 512 * 4 КБ = 2 МБ. То есть Huge Page на уровне PMD, по сути, означает, что запись ссылается на страницу, имеющую тот же размер, что и полный PT.

Расширяя это на уровень PUD, мы просто снова умножаем на 512 и получаем размер полного PMD, содержащего полные PT: 512 * 512 * 4 КБ = 1 ГБ.

PUD страницы Huge Page

~ PUD Entry, Page Size set ~                                     Present ─────┐
                                                             Read/Write ─────┐|
                                                       User/Supervisor ─────┐||
                                                   Page Write Through ─────┐|||
                                                Page Cache Disabled ─────┐ ||||
                                                          Accessed ─────┐| ||||
                                                            Dirty ─────┐|| ||||
┌─── NX                                                Page Size ─────┐||| ||||
|┌───┬─── Memory Protection Key                         Global ─────┐ |||| ||||
||   |┌──────┬─── Ignored                             Ignored ───┬─┐| |||| ||||
||   ||      | ┌─── Reserved           Page Attribute Table ───┐ | || |||| ||||
||   ||      | |┌────────────────────────┐┌───────────────────┐| | || |||| ||||
||   ||      | || 1GB Page Physical Addr ||      Reserved     || | || |||| ||||
||   ||      | ||                        ||                   || | || |||| ||||
XXXX XXXX XXXX 0XXX XXXX XXXX XXXX XXXX XX00 0000 0000 0000 000X XXXX 1XXX XXXX
       56        48        40        32        24        16         8         0

Когда бит Page Size установлен, можно заметить, что запись PUD выглядит больше похожей на запись PT, чем на обычную запись PUD, что логично, ведь она также ссылается на страницу, а не на таблицу страниц.

Однако существуют некоторые отличия от PT:

  1. Бит Page Size находится там, где находится в PT бит Page Attribute Table (PAT), поэтому бит PAT перенесён в бит 12.

  2. Физический адрес 1-гигабайтной Huge Page должен иметь выравнивание в физической памяти по 1 ГБ; именно поэтому существуют новые биты Reserved и поэтому бит 12 можно задействовать как бит PAT.

В целом здесь нет ничего особо нового, при работе Huge Page с другие различия заключаются в том, что для получения физического адреса страницы к адресу нужно применить другую битовую маску; кроме того, выравнивание по 1 ГБ означает, что при вычислении физического адреса виртуального адреса в странице нам нужно использовать маску, основанную на выравнивании по 1 ГБ, а не по 4 КБ.

PMD страницы Huge Page

~ PMD Entry, Page Size set ~                                     Present ─────┐
                                                             Read/Write ─────┐|
                                                       User/Supervisor ─────┐||
                                                   Page Write Through ─────┐|||
                                                Page Cache Disabled ─────┐ ||||
                                                          Accessed ─────┐| ||||
                                                            Dirty ─────┐|| ||||
┌─── NX                                                Page Size ─────┐||| ||||
|┌───┬─── Memory Protection Key                         Global ─────┐ |||| ||||
||   |┌──────┬─── Ignored                             Ignored ───┬─┐| |||| ||||
||   ||      | ┌─── Reserved         Page Attribute Table ─────┐ | || |||| ||||
||   ||      | |┌───────────────────────────────────┐┌────────┐| | || |||| ||||
||   ||      | ||     2MB Page Physical Address     ||Reserved|| | || |||| ||||
||   ||      | ||                                   ||        || | || |||| ||||
XXXX XXXX XXXX 0XXX XXXX XXXX XXXX XXXX XXXX XXXX XXX0 0000 000X XXXX 1XXX XXXX
       56        48        40        32        24        16         8         0

Здесь ситуация очень похожа на запись PUD с установленным битом Page Size; единственное изменение заключается в том. что поскольку на этом уровне выравнивание для 2-мегабайтных страниц меньше, установлено меньше битов Reserved.

Выравнивание по 2 МБ означает, что смещение внутри huge page должно вычисляться при помощи маски, основанной на выравнивании по 2 МБ.

Просматриваем страницы

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

Подготовка

Запустив vm и подключив gdb, я сначала выберу адрес для выполнения просмотра страниц; в качестве примера я использую текущий указатель стека при работе ядра:

gef➤  p/x $rsp
$42 = 0xffffffff88c07da8

Итак, у нас есть адрес для просмотра, давайте получим физический адрес PGD из cr3:

gef➤  p/x $cr3 & ~0xfff
$43 = 0x10d664000

Я воспользуюсь этой небольшой функцией на Python, чтобы извлечь смещения таблиц страниц из виртуального адреса:

def get_virt_indicies(addr):
    pageshift = 12
    addr = addr >> pageshift
    pt, pmd, pud, pgd = (((addr >> (i*9)) & 0x1ff) for i in range(4))
    return pgd, pud, pmd, pt

На выходе получаем следующее:

In [2]: get_virt_indicies(0xffffffff88c07da8)
Out[2]: (511, 510, 70, 7)

PGD

Полученный нами индекс для PGD на основании виртуального адреса — это 511. Умножив 511 на 8, мы получим байтовое смещение в PGD, с которого начинается запись PGD для нашего виртуального адреса:

gef➤  p/x 5118
$44 = 0xff8

Добавив это смещение к физическому адресу PGD, мы получим физический адрес записи PGD:

gef➤  p/x 0x10d664000+0xff8
$45 = 0x10d664ff8

А считывание физической памяти по этому адресу даёт нам саму запись PGD:

gef➤  monitor xp/gx 0x10d664ff8
000000010d664ff8: 0x0000000008c33067

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

Маскирование битов [12, 51) даёт нам физический адрес PUD:

gef➤  p/x 0x0000000008c33067 & ~((1<<12)-1) & ((1ull<<51) - 1)
$46 = 0x8c33000

PUD

Индекс, полученный для PUD на основании виртуального адреса — это 510. Умножив 510 на 8, мы получим байтовое смещение в PUD, с которого начинается запись PUD для нашего виртуального адреса:

gef➤  p/x 5108
$47 = 0xff0

Добавив это смещение к физическому адресу PUD, мы получим физический адрес записи PUD:

gef➤  p/x 0x8c33000+0xff0
$48 = 0x8c33ff0

А считывание физической памяти по этому адресу даёт нам саму запись PUD:

gef➤  monitor xp/gx 0x8c33ff0
0000000008c33ff0: 0x0000000008c34063

На этом этапе нам нужно начать обращать внимание на Size Bit (бит 7), потому что если это 1-гигабайтная страница, мы остановим на этом просмотр страниц.

gef➤  p/x 0x0000000008c34063 & (1<<7)
$49 = 0x0

Похоже, в этой записи от сброшен, так что мы продолжим просмотр страниц.

Также обратим внимание, что запись PUD заканчивается на 0x3, а не на 0x7, как на предыдущем уровне, младшие два бита (present, writeable) по-прежнему установлены, а бит user теперь сброшен. Это значит, что доступ в пользовательском режиме к страницам, принадлежащим к этой записи PUD, приведёт к page fault из-за безуспешной проверки разрешения на доступ.

Бит NX по-прежнему сброшен, поэтому страницы, принадлежащие к этому PUD, по-прежнему могут быть исполняемыми.

Маскирование битов [12, 51) даёт нам физический адрес PMD:

gef➤  p/x 0x0000000008c34063 & ~((1ull<<12)-1) & ((1ull<<51)-1)
$50 = 0x8c34000

PMD

Индекс, полученный нами для PMD на основании виртуального адреса — это 70, поэтому умножение 70 на 8 позволит нам получить байтовое смещение в PMD с которого начинается запись PMD для нашего виртуального адреса:

gef➤  p/x 708
$51 = 0x230

Добавив это смещение к физическому адресу PMD, получим физический адрес записи PMD:

gef➤  p/x 0x8c34000+0x230
$52 = 0x8c34230

А считывание физической памяти по этому адресу даёт нам саму запись PMD:

gef➤  monitor xp/gx 0x8c34230
0000000008c34230: 0x8000000008c001e3

На этом уровне нам тоже нужно обращать внимание на Size Bit, потому что если это страница на 2 МБ, мы остановим на этом просмотр страниц.

gef➤  p/x 0x8000000008c001e3 & (1<<7)
$53 = 0x80

Похоже, наш виртуальный адрес ссылается на 2-мегабайтную Huge Page! Поэтому физический адрес в записи PMD — это физический адрес Huge Page.

Кроме того, судя по битам разрешений, страница по-прежнему Present и Writeable, а бит User по-прежнему сброшен, так что доступ к этой странице есть только из режима supervisor (ring-0).

В отличие от предыдущих уровней, здесь старший бит NX установлен:

gef➤  p/x 0x8000000008c001e3 & (1ull<<63)
$54 = 0x8000000000000000

То есть Huge Page — это не исполняемая память.

Применив битовую маску к битам [21:51), мы получим физический адрес huge page:

gef➤  p/x 0x8000000008c001e3 & ~((1ull<<21)-1) & ((1ull<<51)-1)
$56 = 0x8c00000

Теперь нам нужно применить маску к виртуальному адресу, основанному на 2-мегабайтному выравниванию страниц, чтобы получить смещение в Huge Page.

2 МБ эквивалентно 1<<21, поэтому применив битовую маску (1ull<<21)-1, мы получим смещение:

gef➤  p/x 0xffffffff88c07da8 & ((1ull<<21)-1)
$57 = 0x7da8

Добавив это смещение к базовому адресу 2-мегабайтной Huge Page, мы получим физический адрес, связанный с виртуальным адресом, с которого мы начинали:

gef➤  p/x 0x8c00000 + 0x7da8
$58 = 0x8c07da8

Похоже, виртуальный адрес 0xffffffff88c07da8 имеет физический адрес 0x8c07da8!

Проверка

Есть несколько способов проверки правильного просмотра страниц, легче всего будет просто сдампить память по виртуальному и физическому адресу, а затем сравнить их. Если они похожи, то, вероятно, мы всё сделали правильно:

Физический:

gef➤  monitor xp/10gx 0x8c07da8
0000000008c07da8: 0xffffffff810effb6 0xffffffff88c07dc0
0000000008c07db8: 0xffffffff810f3685 0xffffffff88c07de0
0000000008c07dc8: 0xffffffff8737dce3 0xffffffff88c3ea80
0000000008c07dd8: 0xdffffc0000000000 0xffffffff88c07e98
0000000008c07de8: 0xffffffff8138ab1e 0x0000000000000000

Виртуальный:

gef➤  x/10gx 0xffffffff88c07da8
0xffffffff88c07da8:	0xffffffff810effb6	0xffffffff88c07dc0
0xffffffff88c07db8:	0xffffffff810f3685	0xffffffff88c07de0
0xffffffff88c07dc8:	0xffffffff8737dce3	0xffffffff88c3ea80
0xffffffff88c07dd8:	0xdffffc0000000000	0xffffffff88c07e98
0xffffffff88c07de8:	0xffffffff8138ab1e	0x0000000000000000

На мой взгляд, выглядит неплохо!

Ещё один способ проверки — использовать команду monitor gva2gpa (гостевой виртуальный адрес в гостевой физический адрес), раскрытую gdb благодаря QEMU Monitor:

gef➤  monitor gva2gpa 0xffffffff88c07da8
gpa: 0x8c07da8

Если предположить, что QEMU выполняет трансляцию адресов правильно (вероятно, это справедливое предположение), то у нас есть ещё одно подтверждение успешности просмотра страниц!

Подведём итог

Надеюсь, теперь у вас есть достаточно чёткое понимание того, как работает страничная организация памяти в системах x86_64. Я хотел уместить в пост много информации, поэтому пришлось подумать, как всё это упорядочить, и я всё ещё не уверен, что это идеальный способ.

Как бы то ни было, я считаю страничную организацию памяти довольно удобной; мне кажется, это одна из тех вещей, разобравшись в которых, можно их понять. Но чтобы разобраться, нужно время и изучение gdb.

Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку

Содержание

1.    Лекция
1.

1.1 Виртуальная память

1.2 Реализация виртуальной памяти в
Windows

1.3 Структура виртуального адресного
пространства

1.4 Выделение памяти процессам

2. Лекция 2.

2.1 Трансляция адресов

2.2 Ошибки страниц

2.3 Пределы памяти

Резюме

Контрольные вопросы

3. Лекция 3.

3.1 Программная поддержка

4. Лекция 4.

1.     Лекция 1.

1.1 Виртуальная память

Всем
процессам в операционной системе Windows предоставляется
важнейший ресурс – виртуальная
память
 (virtual memory).
Все данные, с которыми процессы непосредственно работают, хранятся именно в
виртуальной памяти.

Название
«виртуальная» произошло из-за того, что процессу неизвестно реальное
(физическое) расположение памяти – она может находиться как в оперативной
памяти (ОЗУ), так и на диске. Операционная система предоставляет процессу виртуальное адресное
пространство
 (ВАП, virtual address space) определенного размера и процесс может работать
с ячейками памяти по любым виртуальным
адресам этого пространства, не «задумываясь» о том, где реально
хранятся данные.

Размер
виртуальной памяти теоретически ограничивается разрядностью операционной
системы. На практике в конкретной реализации операционной системы
устанавливаются ограничения ниже теоретического предела. Например, для
32-разрядных систем (x86), которые используют для
адресации 32 разрядные регистры и переменные, теоретический максимум составляет 4 ГБ (232 байт = 4 294 967 296 байт =
4 ГБ). Однако для процессов доступна только половина этой памяти – 2 ГБ, другая
половина отдается системным компонентам. В 64 разрядных системах (x64)
теоретический предел равен 16 экзабайт
(264 байт = 16 777 216 ТБ =
16 ЭБ). При этом процессам выделяется 8 ТБ, ещё столько же отдается системе,
остальное адресное пространство в
нынешних версиях Windows не
используется.

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

1.2 Реализация
виртуальной
памяти в Windows

Схема
реализации виртуальной памяти в 32-разрядной операционной системе Windows представлена на 
рис.11.1. Как уже
отмечалось, процессу предоставляется виртуальное адресное пространство размером 4 ГБ, из которых
2 ГБ, расположенных по младшим адресам
(0000 0000 – 7FFF FFFF), процесс может использовать по своему
усмотрению (пользовательское ВАП), а оставшиеся два гигабайта (8000 0000 – FFFF
FFFF) выделяются под системные структуры данных и компоненты (системное ВАП)1.
Отметим, что каждый процесс имеет свое собственное пользовательское ВАП, а
системное ВАП для всех процессов одно и то же.

Реализация виртуальной памяти в 32-разрядных Windows


Рис. 11.1. Реализация виртуальной памяти в 32-разрядных Windows

Виртуальная память делится
на блоки одинакового размера – виртуальные страницы. В Windows страницы
бывают большие (x86 – 4 МБ, x64 – 2 МБ) и
малые (4 КБ). Физическая память (ОЗУ) также делится на страницы точно такого же
размера, как и виртуальная память. Общее
количество малых виртуальных страниц процесса в 32 разрядных системах равно 1
048 576 (4 ГБ / 4 КБ = 1 048 576).

Обычно
процессы задействуют не весь объем виртуальной памяти, а только небольшую его
часть. Соответственно, не имеет смысла (и, часто, возможности) выделять
страницу в физической памяти для каждой виртуальной страницы всех процессов.
Вместо этого в ОЗУ (говорят,
«резидентно») находится ограниченное количество страниц, которые
непосредственно необходимы процессу. Такое подмножество виртуальных
страниц процесса, расположенных в физической памяти, называется рабочим набором
процесса (working set).

Те
виртуальные страницы, которые пока не требуются процессу, операционная система может выгрузить на диск, в специальный файл,
называемый файлом подкачки (page file).

Каким образом
процесс узнает, где в данный момент находится требуемая страница? Для этого
служат специальные структуры данных – таблицы страниц (page
table
).

1.3 Структура виртуального адресного пространства

Рассмотрим,
из каких элементов состоит виртуальное адресное пространство процесса в 32
разрядных Windows.

В
пользовательском ВАП располагаются исполняемый образ процесса, динамически
подключаемые библиотеки (DLLdynamic-link library), куча процесса
и стеки потоков.

При запуске
программы создается процесс (см. лекцию 6 «Процессы и потоки»), при
этом в память загружаются код и данные
программы (исполняемый образ, executable image),
а также необходимые программе динамически подключаемые библиотеки (DLL). Формируется куча (heap) – область, в которой процесс может
выделять память динамическим структурам
данных (т. е. структурам, размер которых заранее неизвестен, а определяется в
ходе выполнения программы). По умолчанию
размер кучи составляет 1 МБ, но при компиляции приложения или в ходе выполнения
процесса может быть изменен. Кроме того, каждому потоку предоставляется стек (stack) для хранения локальных переменных и
параметров функций, также по умолчанию
размером 1 МБ.

Структура виртуального адресного пространства


Рис. 11.2. Структура виртуального адресного пространства

В системном
ВАП расположены:

·                    
образы ядра (ntoskrnl.exe), исполнительной
системы, HAL (hal.dll), драйверов устройств, требуемых при загрузке системы;

·                    
таблицы страниц процесса;

·                    
системный кэш;

·                    
пул подкачиваемой памяти (paged pool) –
системная куча подкачиваемой памяти;

·                    
пул подкачиваемой памяти (nonpaged pool) –
системная куча неподкачиваемой памяти;

·                    
другие элементы (см. [5]).

Переменные, в
которых хранятся границы разделов в системном ВАП, приведены в [
5, стр. 442].
Вычисляются эти переменные в функции MmInitSystem (файл base\ntos\mm\mminit.c,
строка 373), отвечающей за инициализацию подсистемы памяти. В файле
base\ntos\mm\i386\mi386.h приведена структура ВАП и определены константы, связанные с управлением памятью (например,
стартовый адрес системного
кэша MM_SYSTEM_CACHE_START, строка 199).

1.4 Выделение памяти процессам

Существует
несколько способов выделения виртуальной памяти процессам при помощи Windows
API2. Рассмотрим два основных способа – с помощью функции
VirtualAlloc и с использованием кучи.

1.
WinAPI функция VirtualAlloc позволяет
резервировать и передавать виртуальную память процессу.
При резервировании запрошенный диапазон виртуального
адресного пространства закрепляется за процессом (при условии наличия
достаточного количества свободных страниц в пользовательском ВАП),
соответствующие виртуальные страницы становятся зарезервированными (reserved), но доступа к этой памяти у процесса нет –
при попытке чтения или записи возникнет исключение.
Чтобы получить доступ, процесс должен
передать память зарезервированным
страницам, которые в этом случае становятся переданными (commit).

Отметим, что
резервируются участки виртуальной памяти по адресам,
кратным значению константы гранулярности
выделения памяти MM_ALLOCATION_GRANULARITY (файл base\ntos\inc\mm.h,
строка 54). Это значение равно 64 КБ.
Кроме того, размер резервируемой области должен быть кратен размеру страницы (4
КБ).

WinAPI функция VirtualAlloc для выделения памяти
использует функцию ядра NtAllocateVirtualMemory (файл base\ntos\mm\allocvm.c,
строка 173).

2. Для более
гибкого распределения памяти существует куча процесса,
которая управляется диспетчером кучи (heap manager). Кучу используют WinAPI функция HeapAlloc, а также оператор языка C
malloc и оператор C++ newДиспетчер кучи предоставляет возможность
процессу выделять память с
гранулярностью 8 байтов (в 32-разрядных системах), а для обслуживания этих
запросов использует те же функции ядра, что и VirtualAlloc.

Дескрипторы виртуальных адресов

Для хранения
информации о зарезервированных страницах памяти используются дескрипторы
виртуальных адресов (Virtual Address Descriptors, VAD). Каждый дескриптор содержит
данные об одной зарезервированной области памяти и описывается структурой MMVAD
(файл base\ntos\mm\mi.h, строка 3976).

Границы
области определяются двумя полями – StartingVpn (начальный VPN) и EndingVpn (конечный VPN). VPN (Virtual Page Number)
– это номер виртуальной страницы; страницы просто нумеруются, начиная с
нулевой. Если размер страницы 4 КБ (212 байт),
то VPN получается из виртуального адреса
начала страницы отбрасыванием младших 12 бит (или
3 шестнадцатеричных цифр). Например, если виртуальная
страница
 начинается с адреса 0x340000, то VPN такой
страницы равен 0x340.

Дескрипторы
виртуальных адресов для каждого процесса организованы в сбалансированное
двоичное АВЛ дерево3 (AVL tree). Для этого в структуре MMVAD имеются поля
указатели на левого и правого потомков: LeftChild и RightChild.

Для хранения
информации о состоянии области памяти, за которую отвечает дескриптор, в структуре MMVAD содержится поле флагов VadFlags.

2. Лекция 2.

2.1 Трансляция адресов

Трансляция виртуального
адреса – это определение реального
(физического) расположение ячейки памяти с данным виртуальным адресом, т. е.
преобразование виртуального адреса в физический. Принцип трансляции показан
на 
рис.11.1, здесь мы
рассмотрим подробности трансляции и детали реализации в WRK.

Из рис.11.1 видно,
что информация о
соответствии виртуальных адресов физическим хранится в таблицах страниц. В
системе для каждого процесса поддерживается множество записей о страницах: если
размер страницы 4 КБ, то чтобы хранить информацию обо всех виртуальных
страницах в 32 разрядной системе требуется более миллиона записей (4 ГБ / 4 КБ
= 1 048 576). Эти записи о страницах сгруппированы в таблицы страниц (Page Table), запись называется
PTE (Page Table Entry). В каждой таблице
содержится 1024 записи, таким образом, максимальное количество таблиц страниц
для процесса – 1024 (1 048 576 / 1024 = 1024). Половина от общего количества –
512 таблиц – отвечают за пользовательское ВАП, другая половина – за системное
ВАП.

Таблицы страниц хранятся в виртуальной памяти (см. рис.11.2). Информация о
расположении каждой из таблиц страниц находится в каталоге страниц (Page Directory), единственном
для процесса. Записи этого каталога называются PDE (Page Directory Entry). Таким образом,
процесс трансляции является двухступенчатым: сначала по виртуальному адресу определяется запись PDE в
каталоге страниц, затем по этой
записи находится соответствующая таблица страницзапись PTE которой указывает на требуемую
страницу в физической памяти.

Откуда процесс знает, где в памяти хранится каталог страниц? За
это отвечает поле DirectoryTableBase
структуры KPROCESS (файл base\ntos\inc\ke.h,
строка 958, первый элемент массива). Схема трансляции адресов показана на 
рис.11.3.

Трансляция адресов


Рис. 11.3. Трансляция адресов

Записи PDE и PTE представлены структурой MMPTE_HARDWARE (base\ntos\mm\i386\mi386.h, строка 2508),
содержащей следующие основные поля:

·                    
флаг (однобитовое поле) Valid: если виртуальная страница расположена в физической
памяти, Valid = 1;

·                    
флаг Accessed:
если к странице были обращения для чтения, Accessed =
1
;

·                    
флаг Dirty:
если содержимое страницы было изменено (была произведена операция
записи), Dirty = 1;

·                    
флаг LargePage:
если страница является большой (4 МБ), LargePage = 1;

·                    
флаг Owner:
если страница доступна из пользовательского режима, Owner
= 1
;

·                    
20 битовое поле PageFrameNumber:
указывает номер страничного фрейма (PFN, Page Frame Number).

В поле PageFrameNumber
хранится номер записи в базе данных PFN – системной структуре, отвечающей за
информацию о страницах физической памяти. Запись PFN представлена структурой MMPFN (файл base\ntos\mm\mi.h,
строка 1710) и подробно описана в [
5, стр. 502].

2.2 Ошибки
страниц

Страница может находиться либо в физической памяти (ОЗУ), либо на диске в
файле подкачки.

Если в записи PTE флаг Valid = 1, то страница находится в физической
памяти и к ней можно обращаться. Иначе (Valid = 0) – страница недоступна процессу. При
попытке доступа к такой странице возникает страничная ошибка (page fault) и
вызывается функция 
MmAccessFault (файл base\ntos\mm\mmfault.c, строка 101).

Причин страничных ошибок существует множество (см. [Руссинович и
др., 2008, стр. 463]), мы рассмотрим только одну – страница выгружена в
страничный файл (файл подкачки). В
этом случае запись PTE
имеет тип 
MMPTE_SOFTWARE (файл base\ntos\mm\i386\mi386.h,
строка 2446) и вместо поля 
PageFrameNumber имеет 20
разрядное поле 
PageFileHigh, отвечающее за расположение страницы в
страничном файле.

Страничные файлы описываются структурой MMPAGING_FILE (base\ntos\mm\mi.h, строка 4239), имеющей следующие
поля:

·                    
Size –
текущий размер файла (в страницах);

·                    
MaximumSize, MinimumSize –
максимальный и минимальный размеры файла (в страницах);

·                    
FreeSpace, CurrentUsage –
число свободных и занятых страниц;

·                    
PageFileName –
имя файла;

·                    
PageFileNumber –
номер файла;

·                    
FileHandle –
дескриптор файла.

В 32 разрядных Windows поддерживается
до 16 файлов подкачки размером до 4095 МБ каждый. Список
 файлов подкачки находится в ключе реестра
HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\PagingFiles.
Соответствующий системный массив MmPagingFile[MAX_PAGE_FILES] типа PMMPAGING_FILE описывается в файле base\ntos\mm\mi.h (строка 8045).

2.3 Пределы
памяти

В таблицах 8.18.2 и 8.3 приведены
ограничения на виртуальную и физическую память в 32 разрядных и 64 разрядных
операционных системах Windows.

Таблица 11.1. Ограничения на виртуальную память

Тип памяти

32-разрядные Windows

64-разрядные Windows

Виртуальное адресное пространство

4 ГБ

16 ТБ (16 000 ГБ)

Пользовательское ВАП

2 ГБ; до 3 ГБ в случае использования
специальных ключей при загрузке

8 ТБ

Системное ВАП

2 ГБ; от 1 до 2 ГБ в случае
использования специальных ключей при загрузке

8 ТБ

Таблица
11.2. Ограничения на физическую память в клиентских версиях

Версия Windows

32-разрядные

64-разрядные

Windows XP

От 512 МБ (Starter) до 4 ГБ (Professional)

128 ГБ (Professional)

Windows Vista

от 1 ГБ (Starter) до 4 ГБ (Ultimate)

от 8 ГБ (Home Basic) до 128 ГБ (Ultimate)

Windows 7

от 2 ГБ (Starter) до 4 ГБ (Ultimate)

от 8 ГБ (Home Basic) до 192 ГБ (Ultimate)

Таблица 11.3.
Ограничения на физическую память в серверных версиях

Версия
Windows

32-разрядные

64-разрядные

Windows Server 2003 R2

От 4 ГБ (Standard) до 64 ГБ (Datacenter)

От 32 ГБ (Standard) до 1 ТБ (Datacenter)

Windows Server 2008

От 4 ГБ (Web Server) до 64 ГБ (Datacenter)

От 32 ГБ (Web Server) до 1 ТБ (Datacenter)

Windows Server 2008 R2

нет 32 разрядных версий

от 8 ГБ (Foundation) до 2 ТБ (Datacenter)

Резюме

В лекции изучаются такие важные понятия как виртуальная и физическая памятьвиртуальное адресное пространство,
рабочий набор процесса, файл подкачки.
Рассматриваются структура виртуального адресного пространства процесса, способы
выделения памяти процессам, дескрипторы виртуальных адресов, ошибки страниц.
Описывается процесс трансляции виртуальных адресов в физические. Приводятся
ограничения на размер виртуальной и физической памяти в различных версиях Windows.

Следующая лекция посвящена принципам обеспечения безопасности
в Windows.

Контрольные вопросы

·                    
Дайте определение «виртуальной
памяти». Чем виртуальная память отличается от физической?

·                    
Нарисуйте структуру виртуального адресного
пространства в 32 разрядной системе Windows.

·                    
Что такое страница памяти? Какие страницы
используются в Windows?

·                    
Опишите достоинства и недостатки различных
способов выделения памяти.

·                    
Опишите процесс трансляции адресов. Какую
роль в этом процессе играют таблицы страниц? Каталоги страниц?

·                    
Возможно ли в 32 разрядной системе Windows
наличие большего объема физической памяти, чем виртуальной памяти процесса?

Любая
программная система имеет логическую модель памяти. Самая простая из них —
совпадающая с физической, когда все программы имеют прямой доступ ко всему
адресному пространству.
При таком подходе программы имеют доступ ко всему адресному пространству, не
только могут “мешать” друг другу, но и способны привести к сбою работы всей
системы — для этого достаточно, например, затереть кусок памяти, в котором
располагается код ОС. Кроме того, иногда физической памяти может просто не
хватить для того, чтобы все нужные процессы могли работать одновременно.
Виртуальная память — один из механизмов, позволяющих решить эти проблемы. В
данной статье рассматривается работа с этим механизмом со стороны операционной
системы на примере 
ОС Embox.
Все функции и типы данных, упомянутые в статье, вы можете найти в исходном коде
нашего проекта.
Будет приведён ряд листингов, и некоторые из них слишком громоздки для
размещения в статье в оригинальном виде, поэтому по возможности они будут
сокращены и адаптированы. Также в тексте будут возникать отсылки к функциям и
структурам, не имеющим прямого отношения к тематике статьи. Для них будет дано
краткое описание, а более полную информацию о реализации можно найти на вики
проекта.

Общие
идеи
Виртуальная память — это концепция, которая позволяет уйти от использования
физических адресов, используя вместо них виртуальные, и это даёт ряд
преимуществ:

·                    
Расширение реального адресного
пространства. Часть виртуальной памяти может быть вытеснена на жёсткий диск, и
это позволяет программам использовать больше оперативной памяти, чем есть на
самом деле.

·                    
Создание изолированных
адресных пространств для различных процессов, что повышает безопасность
системы, а также решает проблему привязанности программы к определённым адресам
памяти.

·                    
Задание различных свойств для
разных участков участков памяти. Например, может существовать неизменяемый
участок памяти, видный нескольким процессам.
При этом вся виртуальная память делится на
участки памяти постоянного размера, называемые страницами.

Аппаратная
поддержка

MMU — это компонент аппаратного обеспечения
компьютера, через который “проходят” все запросы к памяти, совершаемые
процессором. Задача этого устройства — трансляция адресов, управление
кэшированием памяти и её защита.

Процессор
подаёт на вход MMU виртуальный адрес
Если MMU выключено или если виртуальный адрес попал в нетранслируемую область,
то физический адрес просто приравнивается к виртуальному
Если MMU включено и виртуальный адрес попал в транслируемую область,
производится трансляция адреса, то есть замена номера виртуальной страницы на
номер соответствующей ей физической страницы (смещение внутри страницы
одинаковое):
Если запись с нужным номером виртуальной страницы есть в TLB [Translation
Lookaside Buffer], то номер физической страницы берётся из нее же
Если нужной записи в TLB нет, то приходится искать ее в таблицах страниц,
которые операционная система размещает в нетранслируемой области ОЗУ (чтобы не
было промаха TLB при обработке предыдущего промаха). Поиск может быть
реализован как аппаратно, так и программно — через обработчик исключения,
называемого страничной ошибкой (page fault). Найденная запись добавляется в
TLB, после чего команда, вызвавшая промах TLB, выполняется снова.
Таким образом, при обращении программы к тому
или иному участку памяти трансляция адресов производится аппаратно. Программная
часть работы с MMU — формирование таблиц страниц и работа с ними, распределение
участков памяти, установка тех или иных флагов для страниц, а также обработка page
fault, ошибки, которая происходит при отсутствии страницы в отображении.

В тексте статьи в основном будет рассматриваться
трёхуровневая модель памяти, но это не является принципиальным ограничением:
для получения модели с бóльшим количеством уровней можно действовать
аналогичным образом, а особенности работы с меньшим количеством уровней (как,
например, в архитектуре x86 — там всего два уровня) будут рассмотрены отдельно.

3. Лекция 3.

3.1 Программная поддержка
Для приложений работа с
виртуальной памятью незаметна. Это прозрачность обеспечивается наличием в ядре
ОС соответствующей подсистемы, осуществляющей следующие действия:

·                    
Выделение физических страниц
из некоторого зарезервированного участка памяти

·                    
Внесение соответствующих
изменений в таблицы виртуальной памяти

·                    
Сопоставление участков
виртуальной памяти с процессами, выделившими их

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


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

Виртуальный
адрес.
Page Global Directory (далее — PGD) — таблица (здесь и далее — то же
самое, что директория) самого высокого уровня, каждая запись в ней — ссылка
на Page Middle Directory (PMD), записи которой, в свою очередь,
ссылаются на таблицу Page Table Entry (PTE). Записи
в PTE ссылаются на реальные физические адреса, а также хранят флаги
состояния страницы.
То есть, при трёхуровневой иерархии памяти виртуальный адрес будет выглядеть
так:



Значения полей PGD, PMD и PTE — это индексы в
соответствующих таблицах (то есть сдвиги от начала этих таблиц),
а offset — это смещение адреса от начала страницы.

В зависимости от архитектуры и режима страничной адресации, количество битов,
выделяемых для каждого из полей, может отличаться. Кроме того, сама страничная
иерархия может иметь число уровней, отличное от трёх: например, на x86
нет PMD.

Для обеспечения переносимости мы задали границы этих полей с помощью
констант: MMU_PGD_SHIFT, MMU_PMD_SHIFT, MMU_PTE_SHIFT, которые в
приведённой выше схеме равны 24, 18 и 12 соответственно их определение дано в
заголовочном файле 

src/include/hal/mmu.h. В дальнейшем будет
рассматриваться именно этот пример.

На основании сдвигов PGD, PMD и PTE вычисляются
соответствующие маски адресов.

#define MMU_PTE_ENTRIES    (1UL << (MMU_PMD_SHIFT - MMU_PTE_SHIFT))
#define MMU_PTE_MASK       ((MMU_PTE_ENTRIES - 1) << MMU_PTE_SHIFT)
#define MMU_PTE_SIZE       (1UL << MMU_PTE_SHIFT)
#define MMU_PAGE_SIZE      (1UL << MMU_PTE_SHIFT)
#define MMU_PAGE_MASK      (MMU_PAGE_SIZE - 1)

Эти макросы даны в том же
заголовочном файле.

Для работы с виртуальной таблицами
виртуальной памяти в некоторой области памяти хранятся указатели на все PGD.
При этом каждая задача хранит в себе контекст struct mmu_context,
который, по сути, является индексом в этой таблице. Таким образом, к каждой
задаче относится одна таблица PGD, которую можно определить с
помощью mmu_get_root(ctx).

Страницы и работа с ними
В реальных (то есть не в учебных) системах используются страницы от 512 байт до
64 килобайт. Чаще всего размер страницы определяется архитектурой и является
фиксированным для всей системы, например — 4 KiB.

С одной стороны, при меньшем размере страницы память меньше фрагментируется.
Ведь наименьшая единица виртуальной памяти, которая может быть выделена
процессу — это одна страница, а программам очень редко требуется целое число
страниц. А значит, в последней странице, которую запросил процесс, скорее всего
останется неиспользуемая память, которая, тем не менее, будет выделена, а
значит — использована неэффективно.

С другой стороны, чем меньше размер страницы, тем больше размер страничных
таблиц. Более того, при отгрузке на HDD и при чтении страниц с HDD быстрее
получится записать несколько больших страниц, чем много маленьких такого же
суммарного размера.

Отдельного внимания заслуживают так называемые большие страницы: huge pages и
large pages

[вики].

Платформа

Размер обычной страницы

Размер страницы максимально
возможного размера

x86

4KB

4MB

x86_64

4KB

1GB

IA-64

4KB

256MB

PPC

4KB

16GB

SPARC

8KB

2GB

ARMv7

4KB

16MB


Действительно, при использовании
таких страниц накладные расходы памяти повышаются. Тем не менее, прирост
производительности программ в некоторых случаях может доходить до 10%[
ссылка], что объясняется меньшим размером страничных директорий и
более эффективной работой TLB.

В дальнейшем речь пойдёт о страницах
обычного размера.

Устройство
Page Table Entry
В реализации проекта Embox тип mmu_pte_t — это указатель.
Каждая запись PTE должна ссылаться на некоторую физическую страницу,
а каждая физическая страница должна быть адресована какой-то записью PTE.
Таким образом, в mmu_pte_t незанятыми
остаются MMU_PTE_SHIFT бит, которые можно использовать для сохранения
состояния страницы. Конкретный адрес бита, отвечающего за тот или иной флаг,
как и набор флагов в целом, зависит от архитектуры.

Вот некоторые из флагов:

·                    
MMU_PAGE_WRITABLE — Можно ли
менять страницу

·                    
MMU_PAGE_SUPERVISOR —
Пространство супер-пользователя/пользователя

·                    
MMU_PAGE_CACHEABLE — Нужно ли
кэшировать

·                    
MMU_PAGE_PRESENT —
Используется ли данная запись директории


Снимать и устанавливать эти флаги
можно с помощью следующих функций:

mmu_pte_set_writable(),
mmu_pte_set_usermode(), mmu_pte_set_cacheable(), mmu_pte_set_executable()

Например: mmu_pte_set_writable(pte_pointer,
0)

Можно установить сразу несколько
флагов:

void vmem_set_pte_flags(mmu_pte_t *pte, vmem_page_flags_t flags)


Здесь vmem_page_flags_t — 32-битное
значение, и соответствующие флаги берутся из первых MMU_PTE_SHIFT бит.

Трансляция
виртуального адреса в физический
Как уже писалось выше, при обращении к памяти трансляция адресов производится
аппаратно, однако, явный доступ к физическим адресам может быть полезен в ряде
случаев. Принцип поиска нужного участка памяти, конечно, такой же, как и в MMU.
Для того, чтобы получить из виртуального адреса физический, необходимо пройти
по цепочке таблиц PGD, PMD и PTE.
Функция vmem_translate() и производит эти шаги.
Сначала проверяется, есть ли в PGD указатель на директорию PMD.
Если это так, то вычисляется адрес PMD, а затем аналогичным образом
находится PTE. После выделения физического адреса страницы
из PTE необходимо добавить смещение, и после этого будет получен
искомый физический адрес.

Исходный
код функции vmem_translate
Пояснения к коду функции.
mmu_paddr_t — это физический адрес страницы,
назначение mmu_ctx_t уже обсуждалось выше в разделе “Виртуальный
адрес”.

С помощью функции vmem_get_idx_from_vaddr() находятся
сдвиги в таблицах PGD, PMD и PTE.

void vmem_get_idx_from_vaddr(mmu_vaddr_t virt_addr, size_t *pgd_idx, size_t *pmd_idx, size_t *pte_idx) {
   *pgd_idx = ((uint32_t) virt_addr & MMU_PGD_MASK) >> MMU_PGD_SHIFT;
   *pmd_idx = ((uint32_t) virt_addr & MMU_PMD_MASK) >> MMU_PMD_SHIFT;
   *pte_idx = ((uint32_t) virt_addr & MMU_PTE_MASK) >> MMU_PTE_SHIFT;
}

Работа
с Page Table Entry
Для работы с записей в таблице страниц, а так же с самими таблицами, есть ряд
функций:
Эти функции возвращают 1, если у соответствующей структуры установлен
бит MMU_PAGE_PRESENT

int mmu_pgd_present(mmu_pgd_t *pgd);
int mmu_pmd_present(mmu_pmd_t *pmd);
int mmu_pte_present(mmu_pte_t *pte);

Page
Fault
Page fault — это исключение, возникающее при обращении к странице, которая не
загружена в физическую память — или потому, что она была вытеснена, или потому,
что не была выделена.
В операционных системах общего назначения при обработке этого исключения
происходит поиск нужной странице на внешнем носителе (жёстком диске, к
примеру).
В нашей системе все страницы, к которым процесс имеет доступ, считаются
присутствующими в оперативной памяти. Так, например, соответствующие сегменты
.text, .data, .bss; куча; и так далее отображаются в таблицы при инициализации
процесса. Данные, связанные с потоками (например, стэк), отображаются в таблицы
процесса при создании потоков.
Выталкивание страниц во внешнюю память и их чтение в случае page fault не
реализовано. С одной стороны, это лишает возможности использовать больше
физической памяти, чем имеется на самом деле, а с другой — не является
актуальной проблемой для встраиваемых систем. Нет никаких ограничений, делающих
невозможной реализацию данного механизма, и при желании читатель может
попробовать себя в этом деле :)

4. Лекция 4.

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

void *vmem_alloc_page() {
    return page_alloc(virt_page_allocator, 1);
}


Функция page_alloc() ищет
участок памяти из N незанятых страниц и возвращает физический адрес начала
этого участка, помечая его как занятый. В приведённом коде virt_page_allocator
ссылается на участок памяти, резервированной для выделения физических страниц,
а 1 — количество необходимых страниц.

Выделение таблиц
Тип таблицы (PGD, PMD, PTE)
не имеет значения при аллокации. Более того, выделение таблиц производится
также с помощью функции page_alloc(), только с другим аллокатором (virt_table_allocator).

Участки
памяти (Memory Area)
После добавления страниц в соответствующие таблицы нужно уметь сопоставлять
участки памяти с процессами, к которым они относятся. У нас в системе процесс
представлен структурой task, содержащей всю необходимую информацию для работы
ОС с процессом. Все физически доступные участки адресного пространства процесса
записываются в специальный репозиторий: task_mmap. Он представляет из себя
список дескрипторов этих участков (регионов), которые могут быть отображены на
виртуальную память, если включена соответствующая поддержка.

struct emmap {
     void *brk;
     mmu_ctx_t ctx;
 
     struct dlist_head marea_list;
};
brk — это самый большой из всех физических адресов репозитория, данное значение необходимо для ряда системных вызовов, которые не будут рассматриваться в данной статье.
ctx — это контекст задачи, использование которого обсуждалось в разделе “Виртуальный адрес”.
struct dlist_head — это указатель на начало двусвязного списка, организация которого аналогична организации 
Linux Linked List.

За каждый выделенный участок памяти отвечает структура marea

struct marea {
   uintptr_t start;
   uintptr_t end;
   uint32_t flags;
   uint32_t is_allocated;
 
   struct dlist_head mmap_link;
};
Поля данной структуры имеют говорящие имена: адреса начала и конца данного участка памяти, флаги региона памяти. Поле mmap_link нужно для поддержания двусвязного списка, о котором говорилось выше.
void mmap_add_marea(struct emmap *mmap, struct marea *marea) {
   struct marea *ma_err;
 
   if ((ma_err = mmap_find_marea(mmap, marea->start))
                           || (ma_err = mmap_find_marea(mmap, marea->end))) {
               /* Обработка ошибки */
   }
 
   dlist_add_prev(&marea->mmap_link, &mmap->marea_list);
}

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

int vmem_map_region(mmu_ctx_t ctx, mmu_paddr_t phy_addr, mmu_vaddr_t virt_addr, size_t reg_size, vmem_page_flags_t flags) {
   int res = do_map_region(ctx, phy_addr, virt_addr, reg_size, flags);
 
   if (res) {
               vmem_unmap_region(ctx, virt_addr, reg_size, 0);
   }
 
   return res;
}


В качестве параметров передаётся
контекст задачи, адрес начала физического участка памяти, а также адрес начала
виртуального участка. Переменная flags содержит флаги, которые
будут установлены у соответствующих записей в PTE.

Основную работу на себя берёт do_map_region().
Она возвращает 0 при удачном выполнении и код ошибки — в ином случае. Если во
время маппирования произошла ошибка, то часть страниц, которые успели
выделиться, нужно откатить сделанные изменения с помощью функции
vmem_unmap_region(), которая будет рассмотрена позднее.

Рассмотрим функцию do_map_region() подробнее.

Исходный
код функции do_map_region()

Вспомогательные
макросы
Макросы GET_PTE и GET_PMD нужны для лучшей читаемости кода.
Они делают следующее: если в таблице памяти нужный нам указатель не ссылается
на существующую запись, нужно выделить её, если нет — то просто перейти по
указателю к следующей записи.
В самом начале необходимо проверить, выровнены ли под размер страницы размер
региона, физический и виртуальный адреса. После этого определяется PGD,
соответствующая указанному контексту, и извлекаются сдвиги из виртуального
адреса (более подробно это уже обсуждалось выше).
Затем последовательно перебираются виртуальные адреса, и в соответствующих
записях PTE к ним привязывается нужный физический адрес. Если в
таблицах отсутствуют какие-то записи, то они будут автоматически сгенерированы
при вызове вышеупомянутых макросов GET_PTE и GET_PMD.

Освобождение
виртуального участка памяти (Unmapping)
После того, как участок виртуальной памяти был отображён на физическую, рано
или поздно её придётся освободить: либо в случае ошибки, либо в случае
завершения работы процесса.
Изменения, которые при этом необходимо внести в структуру страничной иерархии
памяти, производятся с помощью функции vmem_unmap_region().

Исходный
код функции vmem_unmap_region()
Все параметры функции, кроме последнего, должны быть уже
знакомы. free_pages отвечает за то, должны ли быть удалены страничные
записи из таблиц.
try_free_pte, try_free_pmd, try_free_pgd — это вспомогательные функции.
При удалении очередной страницы может выясниться, что директория, её
содержащая, могла стать пустой, а значит, её нужно удалить из памяти.

Исходный
код
функций
try_free_pte, try_free_pmd, try_free_pgd


Макросы
вида

#if
MMU_PTE_SHIFT != MMU_PMD_SHIFT

   

#endif


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

Заключение
Конечно, данной статьи не достаточно, чтобы с нуля организовать работу с MMU,
но, я надеюсь, она хоть немного поможет погрузиться в OSDev тем, кому он
кажется слишком сложным.

Страничная память — способ организации виртуальной памяти, при котором виртуальные адреса отображаются на физические постранично. Для 32-битной архитектуры x86 минимальный размер страницы равен 4096 байт.[1]

Поддержка такого режима присутствует в большинстве 32-битных и 64-битных процессоров. Такой режим является классическим для почти всех современных ОС, в том числе Windows и семейства UNIX. Широкое использование такого режима началось с процессора VAX и ОС VMS с конца 1970-х годов (по некоторым сведениям, первая реализация). В семействе x86 поддержка появилась с поколения 386, оно же первое 32-битное поколение.

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

Адрес, используемый в машинном коде, то есть значение указателя, называется «виртуальный адрес».

Адрес, выставляемый процессором на шину, называется «линейный адрес» (который позже преобразуется в физический).

Запись таблицы страниц обычно содержит в себе следующую информацию:

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

Число записей в одной таблице ограничено и зависит от размера записи и размера страницы. Используется многоуровневая организация таблиц, часто 2 или 3 уровня, иногда 4 уровня (для 64-разрядных архитектур).

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

При использовании 3-уровневой организации добавляется надкаталог, хранящий записи, указывающие на несколько каталогов.

Старшие биты виртуального адреса указывают на номер записи в каталоге, средние — номер записи в таблице, младшие (адрес внутри страницы) попадают в физический адрес без трансляции.

Формат записей таблиц, их размер, размер страницы и организация таблиц зависит от типа процессора, а иногда и от режима его работы.

Исторически x86 использует 32-битные PTE, 32-битные виртуальные адреса, 4-КБ страницы, 1024 записи в таблице, двухуровневые таблицы. Старшие 10 битов виртуального адреса — номер записи в каталоге, следующие 10 — номер записи в таблице, младшие 12 — адрес внутри страницы.

Начиная с Pentium Pro, процессор поддерживает страницы размером 4 МБ. Однако, чтобы система и программы, запущенные в ней, могли использовать страницы такого размера, технология 4-МБ страниц (hugepages) должна быть соответствующим образом активирована, а приложение настроено на использование страниц такого размера.

Процессор x86 в режиме PAE (Physical Address Extension) и в режиме x86_64 (long mode) использует 64-битные PTE (из них реально задействованы не все биты физического адреса, от 36 в PAE до 48 в некоторых x86_64), 32-битные виртуальные адреса, 4-КБ страницы, 512 записей в таблице, трёхуровневые таблицы с четырьмя каталогами и четырьмя записями в надкаталоге. Старшие 2 бита виртуального адреса — номер записи в надкаталоге, следующие 9 — в каталоге, следующие 9 — в таблице.
Физический адрес каталога или же надкаталога загружен в один из управляющих регистров процессора.

При использовании PAE вместо 4-МБ больших страниц используются двухмегабайтные. См. также PSE.

В архитектуре x86_64 возможно использовать страницы размером 4 килобайта (4096 байтов), 2 мегабайта, и (в некоторых AMD64) 1 гигабайт.

Если обращение к памяти не может быть оттранслировано через TLB, то микрокод процессора обращается к таблицам страниц и пытается загрузить PTE оттуда в TLB. Если и после такой попытки сохранились проблемы, то процессор исполняет специальное прерывание, называемое «отказ страницы» (page fault). Обработчик этого прерывания находится в подсистеме виртуальной памяти ядра ОС.

Некоторые процессоры (MIPS) не имеют обращающегося к таблице микрокода, и генерируют отказ страницы сразу после неудачи поиска в TLB, обращение к таблице и её интерпретация возлагаются уже на обработчик отказа страницы. Это лишает таблицы страниц требования соответствовать жёстко заданному на уровне аппаратуры формату.

Причины отказа страницы (page fault):

  • не существует таблицы, отображающей данный регион,
  • PTE не имеет взведённого флага «страница отображена»,
  • попытка обратиться из пользовательского режима к странице «только для ядра»,
  • попытка записи в страницу «только для чтения»,
  • попытка исполнения кода из страницы «исполнение запрещено».

Обработчик отказов в ядре может загрузить нужную страницу из файла или же из области подкачки (см. свопинг), может создать доступную на запись копию страницы «только для чтения», а может и возбудить исключительную ситуацию (в терминах UNIX — сигнал SIGSEGV) в данном процессе.

Каждый процесс имеет свой собственный набор таблиц страниц. Регистр «каталог страниц» перегружается при каждом переключении контекста процесса. Также необходимо сбросить ту часть TLB, которая относится к данному процессу.

В большинстве случаев ядро ОС помещается в то же адресное пространство, что и процессы, для него резервируются верхние 1—2 гигабайта 32-битного адресного пространства каждого процесса. Это делается с целью избежать переключения таблиц страниц при входе в ядро и выходе из него. Страницы ядра помечаются как недоступные для кода режима пользователя.

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

Так как память ядра одинакова у всех процессов, соответствующие ей TLB не нужно перегружать после переключения процесса. Для этой оптимизации x86 поддерживает флаг «глобальный» у PTE.

Обработчик отказа страницы в ядре способен прочитать данную страницу из файла.

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

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

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

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

Отображаемые в память файлы используется в ОС Windows, а также ОС семейства UNIX, для загрузки исполняемых модулей и динамических библиотек. Они же используются утилитой GNU grep для чтения входящего файла, а также для загрузки шрифтов в ряде графических подсистем.

Огромным достоинством страничной виртуальной памяти по сравнению с сегментной является отсутствие «ближних» и «дальних» указателей.

Наличие таких концепций в программировании уменьшает применимость арифметики указателей и приводит к огромным проблемам с переносимостью кода с/на такие архитектуры. Так, например, значительная часть ПО с открытым кодом изначально разрабатывалась для бессегментных 32-битных платформ со страничной памятью и не может быть перенесена на сегментные архитектуры без серьёзной переработки.

Кроме того, сегментные архитектуры имеют тяжелейшую проблему SS != DS, широко известную в начале 1990-х годов в программировании под 16-битные версии Windows. Эта проблема приводит к затруднениям в реализации динамических библиотек, ибо они имеют свой собственный DS, и SS текущего процесса, что приводит к невозможности использования «ближних» указателей в них. Также наличие своего собственного DS в библиотеках требует устанавливающих правильное значение DS заплаток (MakeProcInstance) для обратных вызовов из библиотеки в вызвавшее приложение.

Основная статья: Страничный кэш[en]

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

Совершенно очевидно, что данная структура является уже готовым дисковым кэшем. Использование её в качестве кэша также решает проблему когерентности файла, доступного через read/write, и его же, отображённого в память.

Таким образом, пути кэшированного ввода-вывода в дисковый файл (FsRtlCopyRead в Windows и аналогичная ей generic_file_read() в Linux) реализуются как копирования данных в физические страницы, отображенные на файл.

Такая организация кэша является единственной в Windows, эта ОС вообще не имеет классического блочного кэша диска. Метаданные файловых систем кэшируются путём создания лже-файлов (IoCreateStreamFileObject) и создания страничного кэша для них.

Первоначально архитектура x86 не имела флага «страница недоступна на исполнение» (NX).

Поддержка данного флага появилась в архитектуре x86 как часть режима PAE (Physical Address Extension) в поколении Pentium 4, под большим давлением со стороны специалистов по безопасности (см. архивы NTBugTraq). Установка данного флага на страницах стека и кучи (heap) позволяет реализовать аппаратно защиту от исполнения данных, что делает невозможной работу многих разновидностей вредоносного ПО, в том числе, например, злонамеренную эксплуатацию многих брешей в Internet Explorer (брешь декабря 2008 года, см. MS knowledge base, не может быть задействована в случае включенной DEP).

Поддержка PAE в Windows, дающая возможность включения защиты от исполнения данных, появилась в Windows 2000, она включена по умолчанию в серверных версиях Windows и отключена в клиентских.

Устройства PCI, в том числе память видеоплаты, обычно поддерживают только 32-битные адреса. Следовательно, им должны быть выданы физические адреса ниже отметки 4 ГБ. Эта «апертура» уменьшает объём видимой физической памяти ниже отметки 4 ГБ до примерно 3,2 ГБ. Остальная часть физической памяти переотображается контроллером выше отметки 4 ГБ.

Для любого обращения к памяти свыше отметки 4 ГБ (то есть более чем примерно 3,2 ГБ) требуется поддержка контроллером (то есть северным мостом чипсета) такой конфигурации. Современные чипсеты (например, Intel G33) такую поддержку имеют.

Также требуется настройка BIOS под названием memory remapping, отображающая регион [3,2…4] на [4…4,8].

Процессор x86 вне режима PAE использует 32-битные PTE и физические адреса, то есть ему не доступно ничего, находящееся выше отметки 4 ГБ (см. также PSE-36 об одном из вариантов обхода данного ограничения). Таким образом, для использования памяти более, чем примерно 3,2 ГБ в ОС она должна поддерживать PAE. Для Windows — это опция загрузки, для Linux — опция построения ядра.

Кроме того, Microsoft принудительно отключила поддержку физических адресов выше 4 ГБ по политико-маркетинговым соображениям в следующих ОС:

  • 32-битная Windows XP,
  • 32-битный Windows Server 2003 Web Edition,
  • 32-битная Windows Vista (подключение поддержки требует набора команд в командной строке: «BCDEdit /set PAE forceenable», «BCDEdit /set nolowmem on»).

Поддержка физических адресов выше 4 ГБ имеется в следующих версиях:

  • всe 64-битные версии,
  • 32-битная Windows Vista SP1 (поддержка включена по умолчанию, но её подключение нередко может требовать набора команд в командной строке),
  • 32-битный Windows Server 2003, отличный от Web Edition,
  • 32-битный Windows Server 2008.

Таким образом, для того, чтобы использовать память выше 3,2 ГБ в Windows, нужны:

  • поддержка чипсетом,
  • правильные настройки BIOS,
  • правильная версия Windows,
  • правильная опция загрузки (с поддержкой PAE),
  • поддержка 36-битного адресного пространства драйверами устройств.

Тем не менее, даже в «урезанной» версии Windows, не поддерживающей адреса выше 4 ГБ, имеет смысл всегда использовать PAE, ибо (см. выше) защита от исполнения данных (DEP) тоже требует PAE. При включении PAE может перестать работать небольшая часть ПО, например, эмулятор Windows Mobile. Согласно официальной версии Microsoft, введение 4 ГБ ограничения адресного пространства связано с отсутствующей или плохой поддержкой 36-битного адресного пространства некоторыми драйверами устройств, это следует иметь в виду, по причине аппаратных ограничений или неподходящих драйверов невозможно подключить PAE на версиях, имеющих поддержку физических адресов выше 4 ГБ. Возможность включения или выключения PAE не зависит от драйверов, но, если драйвер какого-то старого PCI оборудования не поддерживает правильно физические адреса, не умещающиеся в 32 бита, то данное устройство будет работать неверно и может привести к зависанию всего компьютера.

  • Защищённый режим
  1. Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3 (3A, 3B, 3C & 3D): System Programming Guide (декабрь 2016). Дата обращения: 21 декабря 2019. Архивировано 19 мая 2020 года. (англ.)
  • How It Works: Virtual Memory / David Chisnall, May 21, 2008; Prentice Hall Professional. (англ.)
  • E.2. Страничная организация памяти. Виртуальная память. / Серия: «Основы информационных систем» Выпуск 2 (VII). Память. Юрий А. Денисов
  • Глава 9. Виртуальная память. Архитектурные средства поддержки виртуальной памяти; 9.2.1 Страничная память / Курс Введение в операционные системы, В. Е. Карпов, К. А. Коньков, В. П. Иванников. 2001—2003
  • Лекция 8: Организация памяти компьютера. Простейшие схемы управления памятью. Страничная память / Основы операционных систем, 2004, ISBN 978-5-9556-0044-4

Литература

  • Современные операционные системы, Э. Таненбаум, 2002, СПб, Питер, 1040 стр., (в djvu 10.1Мбайт) подробнее>>
  • Сетевые операционные системы Н. А. Олифер, В. Г. Олифер (в zip архиве 1.1Мбайт)
  • Сетевые операционные системы Н. А. Олифер, В. Г. Олифер, 2001, СПб, Питер, 544 стр., (в djvu 6.3Мбайт) подробнее>>

6.1 Основные понятия

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

Основные методы распределения памяти:

  • Без использования внешней памяти (например: HDD)

  • С использованием внешней памяти

6.2 Методы без использования внешней памяти

6.2.1 Однозадачная система без подкачки на диск

Память разделяется только между программой и операционной системой.

Схемы разделения памяти:

Схемы разделения памяти

Третий вариант используется в MS-DOS. Та часть, которая находится в ПЗУ, часто называется BIOS.

6.2.2 Распределение памяти с фиксированными разделами.

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

Системы могут иметь:

  • общую очередь ко всем разделам

  • к каждому разделу отдельную очередь

Распределение памяти с фиксированными разделами

Недостаток системы многих очередей очевиден, когда большой раздел может быть свободным, а к маленькому выстроилась очередь.

Алгоритмы планирования в случае одной очереди:

  • поочередный

  • выбирается задача, которая максимально займет раздел

Также может быть смешанная система.

6.2.3 Распределение памяти динамическими разделами

В такой системе сначала память свободна, потом идет динамическое распределение памяти.

Распределение памяти динамическими разделами.

Недостатки:

  • Сложность

  • Память фрагментируется

Перемещаемые разделы

Это один из методов борьбы с фрагментацией. Но на него уходит много времени.

Перемещаемые разделы

Рост разделов

Иногда процессу может понадобиться больше памяти, чем предполагалось изначально.

Рост разделов

Настройка адресов и защита памяти

В предыдущих примерах мы можем увидеть две основные проблемы.

  • Настройка адресов или перемещение программ в памяти

  • Защита адресного пространства каждой программы

Решение обоих проблем заключается в оснащении машины специальными аппаратными регистрами.

  • Базовый (указывает начало адресного пространства программы)

  • Предельный (указывает конец адресного пространства программы)

6.3 Методы с использованием внешней памяти (свопинг и виртуальная память)

Так как памяти, как правило, не хватает. Для выполнения процессов часто приходится использовать диск.

Основные способы использования диска:

  • Свопинг (подкачка) — процесс целиком загружается в память для работы

  • Виртуальная память — процесс может быть частично загружен в память для работы

6.3.1 Свопинг (подкачка)

При нехватке памяти процессы могут быть выгружены на диск.

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

Как мы видим процесс А второй раз загрузился в другое адресное пространство, должны создаваться такие условия, которые не повлияют на работу процесса.

Свопер — планировщик, управляющий перемещением данных между памятью и диском.

Этот метод был основным для UNIX до версии 3BSD.

Управление памятью с помощью битовых массивов

Вся память разбивается на блоки (например, по 32бита), массив содержит 1 или 0 (занят или незанят).

Чтобы процессу в 32Кбита занять память, нужно набрать последовательность из 1000 свободных блоков.

Такой алгоритм займет много времени.

битовые массивы и списки

Управление памятью с помощью связных списков

Этот способ отслеживает списки занятых (между процессами) и свободных (процессы) фрагментов памяти.

Запись в списке указывает на:

  • занят (P) или незанят (H) фрагмент

  • адрес начала фрагмента

  • длину фрагмента

Четыре комбинации соседей для завершения процесса X

Алгоритмы выделения блока памяти:

  • первый подходящий участок.

  • следующий подходящий участок, стартует не сначала списка, а с того места на котором остановился в последний раз.

  • самый подходящий участок (медленнее, но лучше использует память).

  • самый неподходящий участок, расчет делается на то, что программа займет самый большой участок, а лишнее будет отделено в новый участок, и он будет достаточно большой для другой программы.

6.3.2 Виртуальная память

Основная идея заключается в разбиении программы на части, и в память эти части загружаются по очереди.

Программа при этом общается с виртуальной памятью, а не с физической.

Диспетчер памяти преобразует виртуальные адреса в физические.

Страничная организация памяти

Страницы — это части, на которые разбивается пространство виртуальных адресов.

Страничные блоки — единицы физической памяти.

Страницы всегда имеют фиксированный размер. Передача данных между ОЗУ и диском всегда происходит в страницах.

Х — обозначает не отображаемую страницу в физической памяти.

Страничное прерывание — происходит, если процесс обратился к странице, которая не загружена в ОЗУ (т.е. Х). Процессор передается другому процессу, и параллельно страница загружается в память.

Таблица страниц — используется для хранения соответствия адресов виртуальной страницы и страничного блока.

Таблица может быть размещена:

  • в аппаратных регистрах (преимущество: более высокое быстродействие, недостаток — стоимость)

  • в ОЗУ

Типичная запись в таблице страниц

Присутствие/отсутствие — загружена или незагружена в память

Защита — виды доступа, например, чтение/запись.

Изменение — изменилась ли страница, если да то при выгрузке записывается на диск, если нет, просто уничтожается.

Обращение — было ли обращение к странице, если нет, то это лучший кандидат на освобождение памяти.

Информация о адресе страницы когда она хранится на диске, в таблице не размещается.

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

Страничная организация памяти используется, и в UNIX, и в Windows.

Хранение страничной памяти на диске

Статическая область свопинга

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

На диске всегда есть дубликат страницы, которая находится в памяти.

Этот механизм наиболее простой.

Статический и динамический методы организации свопинга.

Динамическая область свопинга

Предполагается не выделять страницам место на диске, а выделять только при выгрузке страницы, и как только страница вернется в память освобождать место на диске.

Этот механизм сложнее, так как процессы не привязаны к какому-то пространству на диске, и нужно хранить информацию (карту диска) о местоположении на диске каждой страницы.

Понравилась статья? Поделить с друзьями:
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
  • Эмулятор raspberry pi для windows
  • Как запретить установку конкретного обновления windows 10
  • Fitbit приложение для windows
  • Не запускается установка windows 10 с флешки uefi
  • Windows server 2003 standard oem