Intel x86_64 におけるページング

  • windows
  • x64

序章

Intel x86_64アーキテクチャにはMMU(Memory Management Unit)と呼ばれる、仮想メモリ(Virtual Memory またの名を Linear Memory)と物理メモリ(Physical Memory)の変換を行う機構があります。
MMUはハードウェアレベルでCPUに組み込まれています。

仮想メモリ(Virtual Memory, Linear Memory)

Windows における仮想メモリのアドバンテージの一例をご紹介します。
例えば、WindowsにはコアAPI群があります。kernel32.dllや抽象化レイヤーであるntdll.dllなどです。

Windowsでは、常に様々なプロセスが動作しています。
コアAPI群は常に全てのプロセスのスタートアップ時に読み込まれ、仮想メモリ上にマッピングされます。
しかし、複数のプロセスにわたりそれらの同一モジュールをマッピングするのはリソースの無駄です。
そこで、同じ物理アドレス空間異なる仮想アドレス空間にマッピングすることで、実質的に無駄なリソースを削減することができるわけです。

同じ物理アドレス空間を異なる仮想アドレス空間にマッピング

そのほかに、Windows における共有メモリAPIであるMapViewOfFileがあげられます。
これらのAPIは内部的には、同じ物理メモリアドレス空間を異なる仮想アドレス空間にマッピングすることで実現しています。

詳細な仮想メモリ テクノロジーについては、別の記事にてご紹介します。

Intelの実装

ここからは、実際のIntel x86_64におけるページングの実装を見ていきます。

page table definition

Intel x86_64 における仮想アドレスは、実際には48 bitsで構成されています。

PML4PDPPDPTそれぞれ9 bitsです。

  • PML4 = Page Map Level 4
  • PDP = Page Directory Poiner
  • PD = Page Directory
  • PT = Page Table

64-bitである仮想アドレスが実際には48-bitしか使われていない理由については、順を追って後述します。

PML4 - Page Map Level 4

Page Map Level 4として知られるPML4は下記のように定義されています。
paging-definitions.h#L23

typedef struct
{
  uint64 present                   :1;
  uint64 writeable                 :1;
  uint64 user_access               :1;
  uint64 write_through             :1;
  uint64 cache_disabled            :1;
  uint64 accessed                  :1;
  uint64 ignored_3                 :1;
  uint64 size                      :1; // must be 0
  uint64 ignored_2                 :4;
  uint64 page_ppn                  :28;
  uint64 reserved_1                :12; // must be 0
  uint64 ignored_1                 :11;
  uint64 execution_disabled        :1;
} __attribute__((__packed__)) PageMapLevel4Entry;

PDP - Page Directory Poiner

Page Directory Poinerとして知られるPDPは下記のように定義されています。
paging-definitions.h#L40

struct PageDirPointerTablePageDirEntry
{
  uint64 present                   :1;
  uint64 writeable                 :1;
  uint64 user_access               :1;
  uint64 write_through             :1;
  uint64 cache_disabled            :1;
  uint64 accessed                  :1;
  uint64 ignored_3                 :1;
  uint64 size                      :1; // 0 means page directory mapped
  uint64 ignored_2                 :4;
  uint64 page_ppn                  :28;
  uint64 reserved_1                :12; // must be 0
  uint64 ignored_1                 :11;
  uint64 execution_disabled        :1;
} __attribute__((__packed__));

PD - Page Directory

Page Directoryとして知られるPDは下記のように定義されています。
paging-definitions.h#L83

struct PageDirPageTableEntry
{
  uint64 present                   :1;
  uint64 writeable                 :1;
  uint64 user_access               :1;
  uint64 write_through             :1;
  uint64 cache_disabled            :1;
  uint64 accessed                  :1;
  uint64 ignored_3                 :1;
  uint64 size                      :1; // 0 means page table mapped
  uint64 ignored_2                 :4;
  uint64 page_ppn                  :28;
  uint64 reserved_1                :12; // must be 0
  uint64 ignored_1                 :11;
  uint64 execution_disabled        :1;
} __attribute__((__packed__));

PT - Page Table

Page Tableとして知られるPDは下記のように定義されています。
paging-definitions.h#L126

typedef struct
{
  uint64 present                   :1;
  uint64 writeable                 :1;
  uint64 user_access               :1;
  uint64 write_through             :1;
  uint64 cache_disabled            :1;
  uint64 accessed                  :1;
  uint64 dirty                     :1;
  uint64 size                      :1;
  uint64 global                    :1;
  uint64 ignored_2                 :3;
  uint64 page_ppn                  :28;
  uint64 reserved_1                :12; // must be 0
  uint64 ignored_1                 :11;
  uint64 execution_disabled        :1;
} __attribute__((__packed__)) PageTableEntry;

PML4, PDP, PD, PT 対応するメンバ変数

PML4 PDP PD PTそれぞれに対応するメンバ変数は下記のようになっています。

  • present
    • エントリのマッピングの有効性を示す
  • writeable
    • ページへのアクセス制御のフラグです。0になっている状態で書き込みを試みるとPageFault(ページフォールト)が発生する
  • user_access
    • 命名の通り、ユーザーモードからのこのページへのアクセスの可否を示す
  • write_through
    • キャッシングストラテジであるwrite-throughもしくはwrite-backを示す
  • cache_disabled
    • ページのキャッシュ可否を示す
  • accessed
    • ページの読み/書き時にMMUによってフラグが立てられる
  • page_ppm
    • PDPの物理ページ番号
  • dirty
    • MMUによって書き込み時にフラグが立てられる
  • page_ppn
    • マップされた仮想アドレスの物理ページ番号を保持(ページの物理アドレスを4096で割ったもの)
  • reserverd, global_page, ignored
    • PDによって使用されない

ページングモード

モダンなCPUのページングモードには、大きく分けて下記のようなものがあります。

  • Long モード
  • Legacy モード

Longモードはモダンなx64 OSで使用されており、x64 Windowsでは基本的にロングモードです。
一方でLegacyモードはその名前の通り、レガシーな16-bit および 32-bit OSで使われています。

CR4.PAE - 物理アドレス拡張 (Physical Address Extension)

コントロールレジスタのひとつであるCR4PAE(Physical Address Extension)(5番目のビット)にはPAEの有効性を示すフラグが存在しています。
PAEはIntel IA-32において4GB以上のメモリを扱えるようにする技術です。

Intel開発者マニュアルFigure 2-7. Control Registersにはこのように記載されています。

Intel CR4

出典: Intel® 64 and IA-32 Architectures Software Developer’s Manual

CR4.PAE
Physical Address Extension (bit 5 of CR4) — When set, enables paging to produce physical addresses with more than 32 bits. When clear, restricts physical addresses to 32 bits. PAE must be set before entering IA-32e mode.
See also: Chapter 4, “Paging.”

Windows 10 x64環境下では、実際に下記のように確認できます。

DbgView CR4.PAE

CR0.PG

コントロールレジスタのひとつであるCR0PG(Paging)(31番目のビット)にはページングの有効フラグが設定されています。 フラグが立っていれば、CPUはページングモードを使用します。

Intel開発者マニュアルFigure 2-7. Control Registersにはこのように記載されています。

CR0.PG
Paging (bit 31 of CR0) — Enables paging when set; disables paging when clear. When paging is disabled, all linear addresses are treated as physical addresses. The PG flag has no effect if the PE flag (bit 0 of register CR0) is not also set; setting the PG flag when the PE flag is clear causes a general-protection exception (#GP). See also: Chapter 4, “Paging.”
On Intel 64 processors, enabling and disabling IA-32e mode operation also requires modifying CR0.PG.

Intel CR0

出典: Intel® 64 and IA-32 Architectures Software Developer’s Manual

Windows 10 x64環境下では、実際に下記のように確認できます。

DbgView CR0

Program CR0

UINT64 cr0 = __readcr0();
BOOLEAN PagingEnabled = cr0 & 0x31;

__readcr0の実行にはカーネルモード特権が必要です。
上記のプログラムのソースコードについてはこちらのGithubよりご覧いただけます。

仮想アドレス↔物理アドレスへの変換

Page Table Overview

上記図が示す通り、仮想アドレスにマップされている物理アドレスを取得するには、

  1. PML4エントリが保持するPDPの物理アドレスを取得
  2. PDPエントリが保持するPDの物理アドレスを取得
  3. PDエントリが保持するPTの物理アドレスを取得
  4. 最終的に、PTは物理アドレスページを示します。
  5. PTが取得した物理アドレスページ + offsetがその仮想アドレスがマップされている物理アドレスを示します。

上記の手順にのいずれかにおいて失敗するとPageFault(ページフォールト)が発生します。
また、これらの手順は4KBの物理ページの最終地点に到達するまで続けられます。

一般的なx64システムにおいて、ページは4KB(4096 bytes)のサイズです。
各テーブルにおいてそれぞれが2^[9(bits)] = 512のエントリを保持します。

WinDbgの!vtop [pml4.base] [virtual]コマンドを使用して、仮想アドレスにマップされている物理アドレスを参照してみましょう。
vtop -> virtual to physical

Windbg vtop

この場合、カーネル仮想アドレス0xffff94814ee88080には物理アドレス0x7ca88080がマップされていることがわかりました。

CR3 - Page Map Base Register

CR3には、PML4テーブルのベース物理アドレスが格納されています。

CR3 PML4 Base Physical Address

物理アドレスのメモリダンプは下記のようになっています。
ddは通常仮想メモリをダンプします。したがって、物理メモリのダンプには!をプレフィクスとして付与する必要があることに注意してください。

CR3 Physical Memory Dump

本例では、PML4テーブルのベースアドレスは0x1ad000ですね。
PID == 4であるシステムプロセスのDirBaseも同様の物理アドレスを示していることが確認できます。

Process DirBase

Windowsでは、システムプロセスは常にPID == 4です。
同じくして、Windows プロセスの構造体であるEPROCESSを覗いてみましょう。

Process Structure EPROCESS

DirectoryTableBase(KPROCESS)EPROCESS->Pcbに位置しています。

Process Structure KPROCESS

16-bitはどこへ

最初に、64-bitの仮想アドレスは実際には48-bitしか使われていないということを述べました。
これは、MMUの制限によるものです。

もし、64ビットをフルに活用できると仮定すると、合計約16EB(エクサバイト)
つまりは1844京6744兆0737億0955万1616通りものアドレスを示すことができると言えます。

これは、MMUが仮想アドレスを物理アドレスへ変換する過程において問題になります。
また、システムはそれだけの仮想メモリ範囲をサポートできません。

CPUはGovernor(ガバナー)と呼ばれる機能の一部として、64-bit仮想アドレスを意図的に48-bitに制限しています。
近代の多くのCPUでサポートされている最大仮想メモリが256TBなのは、こういった事情が背景としてあるということです。

アドレスの正規化 - Canonical Addressing

48-bit アドレッシングにおいては、63-47の16-bitは47ビット目の値によって決まります。
例えば、47ビット目が1であれば、63-47の全てのビットは1になります。
同じく、47ビット目が8であれば、63-47の全てのビットは8になります。

Canonical Addressing

以下に、Windows 10 x64での一例を示します。
Windows にてアプリケーションを開発・デバッグしたことがある方には馴染みの深いアドレスかも知れません。

ユーザーモードでは、例えば、kernel32.dllのエクスポートするCloseHandleの仮想アドレスは本例では0x00007ff8fd6d48e0です。

kernel32.dll - CloseHandle VA

上位16ビットを見てみると、47ビット目の0となっていることがわかります。

kernel32.dll - CloseHandle VA


一方で、カーネルにおける仮想アドレスを見てみましょう。
この場合、ntoskrnlのエクスポートするNtCreateFileの仮想アドレスは本例では0xfffff8011a880cb0です。

NT - Kernel VA

47ビット目が1となっているため、16ビット全て1となっていることが確認できますね。

NT - Kernel VA

Connor McGarrはここで、ひとつの”質問”を投げかけています。

Question to you, the reader. Now that we know 64-bit systems only utilize 48 bits, do you see a clear need for 128-bit processors in the near future?

結論から言えば、128ビットのプロセッサの登場は現実的ではないでしょう。
その理由は、ここまでの事柄を理解していれば自然と理解できているはずです。

参照

Special thanks to the great-article authors following: