[x64] MTRRの代わりにPage Attribute TableでWrite Combiningを使う

# 概要
自作OSの製作に挑戦していると、フレームバッファ(VRAM)などへの書き込みで、必ず著しい書き込み速度の低下に出会う事かと思います。近年(?)のUEFI BIOSによっていきなり64Bitモードでページング機構が有効な状態から自作OSをブート出来る時代になりましたので、そのページング機構に備わっているPage Attibute Table機能を使ってWrite Combiningを実現し、フレームバッファへの書き込みを高速化してみます。

詳しくは、Intel 64 and IA-32 Architectures Software Developer's Manual Volume 3Aの「11.12 PAGE ATTRIBUTE TABLE (PAT)」を参照して下さい。

また、この記事内では以下の項目を前提としています：

* 呼び出し規約は**Microsoft x64 ABI**
* ページマッピングは**Identity Mapping**
* シングルプロセッサ

# Page Attibute Tableとは
Page Attibute Tableとは、ページング機構で使用するページエントリー(PDPTE,PDE,PTE)で設定される**3つのページ属性ビット(PAT,PCD,PWT)**の働きをカスタマイズする機能です。

## CPUがPage Attribute Tableに対応しているかを確認する
EAX=1でCPUID命令を呼び出して、EDX[bit 16]を確認します。EDX[bit 16]が立っていればPage Attribute Tableに対応しています。

```:NASM 
;extern "C" bool IsPATSupported();
GLOBAL IsPATSupported
IsPATSupported:
	push rbx            ; save non-volatile register
	mov eax, 1
	cpuid
	and edx, 0x00010000 ; check bit 16 
	shr edx, 16         ; edx >> 16
	mov eax, edx
	pop rbx             ; restore non-volatile register
	ret
```


## Page Attribute Tableを取得/設定する
Page Attribute **Table**と言うくらいなので、この機能にはテーブルが存在します。このテーブルは大きさが8バイトで、上述した3つのビット**PAT,PCD,PWT**の組み合わせに対応したメモリの属性を表す値が1バイト*8つ格納されており、それぞれPA0～PA7と定義されています。

また、このテーブルはMSR(Model Specific Register)のIA32_PAT(0x0227)に格納されているため、読み書きのためにRDMSR命令とWRMSR命令を使う必要があります。

| IA32_PAT MSR | PAx | PAT | PCD | PWT | default |
|:-:|:-:|:-:|:-:|:-:|:-:|
| IA32_PAT[7:0] | PA0 | 0 | 0 | 0 | WB |
| IA32_PAT[15:8] | PA1 | 0 | 0 | 1 | WT |
| IA32_PAT[23:16] | PA2 | 0 | 1 | 0 | UC- |
| IA32_PAT[31:24] | PA3 | 0 | 1 | 1 | UC |
| IA32_PAT[39:32] | PA4 | 1 | 0 | 0 | WB |
| IA32_PAT[47:40] | PA5 | 1 | 0 | 1 | WT |
| IA32_PAT[55:48] | PA6 | 1 | 1 | 0 | UC- |
| IA32_PAT[63:56] | PA7 | 1 | 1 | 1 | UC |

※それぞれ下位3ビットが有効で、それ以外は0でなければいけません。


今回は、PA4がWC(Write Combine)属性を示す様に設定します。

```C++:Code.1
constexpr UINT32 MSR_IA32_PAT = 0x0000'0227; 
constexpr UINT8 MEMORY_TYPE_WC = 0x01; // Write Combine

if (IsPatSupported()) {
    auto pat = RdMsr(MSR_IA32_PAT);
    reinterpret_cast<UINT8*>(&pat)[4] = MEMORY_TYPE_WC;
    WrMsr(MSR_IA32_PAT, pat);

    // Code.1-2 (後述)
}
```

<details><summary>RdMsr, WrMsrの実装</summary><div>

```:NASM
; extern "C" UINT64 RdMsr(UINT32 ecx);
GLOBAL RdMsr
RdMsr:
    rdmsr
    shl rdx, 32
    or rax, rdx   ; RAX[63:32] <= EDX, RAX[31:0] <= EAX
    ret    

; extern "C" void WrMsr(UINT32 ecx, UINT64 value);
GLOBAL WrMsr
WrMsr:
    mov eax, edx  ; EAX <= value[31:0]
    shr rdx, 32   ; EDX <= value[63:32]
    wrmsr
    ret
```

</div></details>

# ページエントリーの属性を設定する

最後に、Write Combine(WC)にしたいページのページエントリーの3つの属性ビットをPAT=1,PCD=0,PWT=0にすることでそのページのメモリ属性がPA4を指し示す様にします。今回はフレームバッファに充てられているページをWrite Combineにします。

Intelのマニュアルによると、キャッシュ可能なページをWrite Combineなページに変更する場合は、次の様に一手間掛かる様です：

1. 対象ページのマッピングを解除する
2. 以前の(解除前の)マッピングを使ったかもしれないプロセッサのTLBをフラッシュする
3. Write Combineとなるように属性ビットを設定して新しくマッピングを行う
4. 以前のマッピングを使ったかもしれないプロセッサのキャッシュをフラッシュする

```C++:Code.1-2
	auto base = reinterpret_cast<UINT64>(Param.FrameBuffer.Base);
	auto end = base + Param.FrameBuffer.Size;

	// ページエントリーがあるページが書き込み禁止になっていることがあるのでCR0.WP=0にする
	SetWriteProtection(false);

	auto cr3 = GetCR3();
	auto pPml4 = reinterpret_cast<PAGE_ENTRY*>(cr3.Bits.PageDirectoryBase << PAGEBITS);

	// 一度、マッピングを解除する
	for (auto adrs = base; adrs < end; ) {
		auto l4i = PML4_INDEX(adrs);
		auto l3i = PDPT_INDEX(adrs);
		auto l2i = PD_INDEX(adrs);
		auto l1i = PT_INDEX(adrs);

		auto pPdpt = reinterpret_cast<PAGE_ENTRY*>(pPml4[l4i].PML4.Address << PAGEBITS);
		if (!pPdpt[l3i].PDPTE1G.Large) {
			auto pPd = reinterpret_cast<PAGE_ENTRY*>(pPdpt[l3i].PDPTE.Address << PAGEBITS);
			if (!pPd[l2i].PDE2M.Large) {
				auto pPt = reinterpret_cast<PAGE_ENTRY*>(pPd[l2i].PDE.Address << PAGEBITS);
				pPt[l1i].PTE.P = 0;
				adrs += PAGESIZE;
			}
			else {
				pPd[l2i].PDE2M.P = 0;
				adrs += PAGESIZE2M;
			}
		}
		else {
			pPdpt[l3i].PDPTE1G.P = 0;
			adrs += PAGESIZE1G;
		}
	}

	// TLBをクリアする
	FlushTLB();

	// 再度、マッピングを行う
	for (auto adrs = base; adrs < end; ) {
		auto l4i = PML4_INDEX(adrs);
		auto l3i = PDPT_INDEX(adrs);
		auto l2i = PD_INDEX(adrs);
		auto l1i = PT_INDEX(adrs);

		auto pPdpt = reinterpret_cast<PAGE_ENTRY*>(pPml4[l4i].PML4.Address << PAGEBITS);
		if (!pPdpt[l3i].PDPTE1G.Large) {
			auto pPd = reinterpret_cast<PAGE_ENTRY*>(pPdpt[l3i].PDPTE.Address << PAGEBITS);
			if (!pPd[l2i].PDE2M.Large) {
				auto pPt = reinterpret_cast<PAGE_ENTRY*>(pPd[l2i].PDE.Address << PAGEBITS);
				auto v = pPt[l1i];
				v.PTE.P = 1;
				v.PTE.PAT = 1;
				v.PTE.PCD = 0;
				v.PTE.PWT = 0;
				pPt[l1i] = v;
				adrs += PAGESIZE;
			}
			else {
				auto v = pPd[l2i];
				v.PDE2M.P = 1;
				v.PDE2M.PAT = 1;
				v.PDE2M.PCD = 0;
				v.PDE2M.PWT = 0;
				pPd[l2i] = v;
				adrs += PAGESIZE2M;
			}
		}
		else {
			auto v = pPdpt[l3i];
			v.PDPTE1G.P = 1;
			v.PDPTE1G.PAT = 1;
			v.PDPTE1G.PCD = 0;
			v.PDPTE1G.PWT = 0;
			pPdpt[l3i] = v;
			adrs += PAGESIZE1G;
		}
	}

	FlushCache();

	// Enable write-protection
	SetWriteProtection(true);
```

<details><summary>上記コードで呼ばれている関数</summary><div>

```C++:Code.2
void SetWriteProtection(bool enabled)
{
	auto cr0 = GetCR0();
	cr0.Bits.WP = enabled;
	SetCR0(&cr0);
}
```
```:NASM
; extern "C" void FlushTLB();
GLOBAL FlushTLB
FlushTLB:
	mov rax, cr3
	mov cr3, rax
	ret

; extern "C" void FlushCache();
GLOBAL FlushCache
FlushCache:
    wbinvd
    ret
```

FlushTLB()やFlushCache()はこの記事を書くにあたって適当に実装したので、マルチコア/プロセッサ環境下やCR4.PCIDE==1などの条件下や他の場面では不適切かもしれません。

</div></details>

# 実機での動作確認

* Dell Inspiron 15
* CPU: Pentium 2127U 1.90 GHz
* メモリー: 8 GB
 
手元の実機環境では、とりあえずこの実装で体感出来るくらいに改善しました。
画面表示の高速化は、まだスクロール時にフレームバッファからデータを読み込んでいたり、memcpy()自体の改善も残っていますが、PATだけでも実機テスト時のデバッグ情報を出力する待ち時間が減って快適になりました。

# 最後に

まだページング機構の理解も浅く、メモリ管理周りもまだ書いてないので、もっと良い実装があるかもしれません。載せているコードは実際に使っているものですが、疑似コードくらいのつもりで読んで頂けたら幸いです。参考になったらもっと幸いです。ここまで読んで頂きありがとうございました。