В этом посте я буду говорить о страничной организации только в контексте 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:
-
Бит Page Size находится там, где находится в PT бит Page Attribute Table (PAT), поэтому бит PAT перенесён в бит 12.
-
Физический адрес 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.
Автор:
PatientZero