Above 4G Decoding を無効化しないと動かないPCI(e)デバイスを QEMU 上の UEFI 環境で動かす

# まえがき

「Above 4G Decoding を無効化しないと動かないPCI(e)デバイス」というのが世の中には存在する。Linuxでは lspci 、Windows ではデバイスマネージャーでも特に異常はないが、正常に稼働させるには「BIOS/UEFIの設定で Above 4G Decoding を無効化する」という回避策を取らないと行けない場合がある。私が知っている範囲ではこの事象はドライバが原因で起きており、究極的にはそれを改良すれば問題は解決する。だが、ドライバと言うのはベンダー公式のものはたいていソースコードがクローズである。それに、もし公開されていたとしても、 Windows 64-bit の場合には電子署名が必要であり、オレオレビルドのドライバを動かすのにも TESTSIGNING ブートオプションを有効化しなければならず、大変にめんどくさい。~~いやまあどう考えてもこの後説明するUEFIファームウェア再構築のほうがだるいだろとは思うが。~~

https://docs.microsoft.com/ja-jp/windows-hardware/drivers/install/the-testsigning-boot-configuration-option

本記事ではそんなデバイスを Above 4G Decoding に相当する機能が勝手に有効で動いてしまう QEMU + UEFI(OVMF) の環境でも動かせる、怪しいノウハウをメモる。

「そんなデバイス見たことないのだが？」という人は、安心してそっ閉じしてほしい。だが、一定数心当たりがある人には役立つと嬉しいノウハウがあるかもしれない。

# Above 4G Decoding とは

まずは、 Above 4G Decoding に関して軽く解説する。PCI及び後継のPCI Expressにおいて、ホスト(CPU)と各種デバイスの通信は、大きく I/O   ポートに対する直接通信と、Memory mapped I/O に分けられる。諸般の事情で主たる高速な通信において大抵は Memory mapped I/O を使う。以下、Memory mapped I/Oは長いので略称のMMIOと書く。

## MMIO

MMIO とは、ほぼ名前通り IO の読み書きをメモリの所定アドレスの読み書きにマッピングする手法だ。**事前にアドレスの範囲を各デバイスに割り当て**、その範囲に読み書きする命令がCPU上で発行されたら、そのデバイスへの読み書きとしてコンピューター振る舞う。

問題となるのは太字にした 「**事前にアドレスの範囲を各デバイスに割り当て**」のところだ。 64-bit 時代を生きる我々にとって、もはや昔話ではあるのだが、2006年にWindows Vistaが出るまで、ほとんどのパソコンが32-bitの世界だった。当然、その時点でPCIもPCI Expressも存在した。MMIOには物理アドレス空間を必要とする。32-bitのアドレス空間というのは4GiBであり、32-bitの時代はこの空間内にPCI(e)デバイスをマッピングしていた。ご存じのように(?)通常の 32-bit マシンで 32-bit OS を動かしていると、4GiBの物理メモリ(DRAM)を搭載しても3GiB弱しか認識しなかった。これは空間の一部がこのMMIO用に確保されており、物理的にはDRAMは残っているけど物理アドレス空間にマッピングできないので、OSすら扱えないという問題によるものだった。過渡期にはいろいろ対応策があったのだが、話がそれるので無視する。

さて、この割り当てなのだが、概ね以下のような流れで行う。以下UEFIを前提とし、関係ないステップはとにかく省略する。

1. パソコン起動
2. UEFIファームウェア読み込み、起動
3. UEFIファームウェアがPCI(e)バスを走査してPCI(e)デバイスやPCIブリッジ、PCI(e)スイッチなどをenumeration(列挙)する。この際にBAR(後述)の情報を集める。
4. UEFIファームウェアがenumerationしたBARに対してメモリアドレスを割り当てる。
5. UEFIファームウェアが割り当てたメモリアドレスをBARに書き戻す。
6. OSが起動する(この時ACPI MCFGを介してMMIOのメモリ割り当てを引き渡す)
7. OS + Driver が割り当てられたアドレスに対して読み書きをする、とHW側でマッピングしてくれる。

という流れである。なお、今回はガン無視するがPCI(e)以外にもメモリアドレスにマップされるものがあり、まあ割とかつかつともいう。

ここで登場するBAR(Base Address Register)というのが、MMIOにおいて重要になってくる。

## BAR

PCIデバイス(以降、これにはPCI Expressデバイスも含む)には Configuration Space Header という、ホストがPCIデバイスを扱うために必要なデバイスIDなどと言った構成情報を補完する領域がある。

https://www.macnica.co.jp/business/semiconductor/articles/microchip/140352/

こちらの記事なども参考に。

PCIデバイスの場合(そうではないのが、ブリッジやスイッチの類だがその話は省略)ここの領域の 0x10 バイト目からBARの領域が始まる。一つのBARのサイズは 32-bit の場合と 64-bit の場合とがある。BARのサイズが 32-bit か 64-bit かは、該当部分のワード(32-bit)を読み出し、その 2,3Bit目 を見ることで判断できる。余談だが、ここで少々注意が必要なのはBARは、いわゆる主記憶や汎用レジスタのようなCPUの支配下にある記憶域と異なり、通信手段であるという点である。この意味するところは、あるアドレスに対して書き込んだ直後に読み出したとしても内容が一致するとは限らず、まさにBARもそのような振る舞いが規定されている。

BARの最初の読み出しでBAR自体が 32-bit か 64-bit かわかったら、それを「元の値」として保持しておき、同じ領域 32-bit を全部1で埋める。つまり0xffffffffを書き込む。その直後に再度同じ領域を読み出すと、そこのBAR向けに確保するべきメモリ空間の大きさを示す値が入っている。BARがない、つまりもう必要なMMIO領域がないというときには0となる。具体的な細かいフォーマットは長くなるのでここでは述べないが、要は所定のパターンを書き込むことで、必要なメモリ空間のサイズをデバイスが教えてくれるのだ。その後は退避しておいた「元の値」を書き込む。

その後、もしBARが 32-bit 幅のものであるならば、読み出す先を 32-bit ずらす、すなわち 0x10 → 0x14 のように変化させ、 64-bit 幅のものであったら 0x10 → 0x18 のように変化させ、同じ作業をする。この繰り返しを 0x28 未満の領域を有効なメモリ空間のサイズを返してくる BAR がある限り繰り返しスキャンしていく。

これをすべてのデバイスに繰り返し行うと、全体として必要な PCI MMIO 領域の数と大きさがわかる。これが、いわゆる PCI enumeration の大きな流れである。その後、具体的にどのアドレスを割り振るかを何らかのアルゴリズムで決定し、それぞれの BAR に対応する Base Address 、すなわちメモリ空間における割り当てられた領域の先頭アドレスを(下位 4-bit を除き)格納する。(メモリはアライメントされている必要があるので、どちらにせよ下位 4-bit は常に 0 なので意味はなく、これできちんとうまくいく)

もし、BARが64-bit幅を指示しているならば、 enumeration でスキップした次の 32-bit が Base Address の上位32-bit分の格納場所になる。なお、これはこのデバイスには 64-bit に収まる範囲でメモリ範囲を指定できますよ、という話で、必ずしも 32-bit を超えた領域を使う必要はなく、その判断はデバイスではなく、ホスト側(今回はUEFIファームウェアに相当する)が判断することになる。

## 32-bit の時代

32-bit の時代の話をすると、(と言いつつ、今も結構なパソコンが同じ動作をするが)PCIデバイスの BAR が 32-bit だろうが 64-bit だろうが、ストレートに用意できるメモリ空間がそもそも 32-bit なので、有効な BAR がある位置に 32-bit のメモリ空間から割り当てれば良い。大変シンプルだ。

だが、 32-bit のメモリ空間は全く他の用途を無視したとしても 4GiB しかない。 PCI Express デバイスとして代表的であるGPUには、コンシューマー向けでも 4GiB は愚か、2桁GiBクラスのメモリを搭載しているケースがある。このGPUのメモリ、すなわちVRAM全体にMMIOをしようとしたら、全くもって空間が足りないということになる。また、MMIOを必要とするデバイスがかなり多いということも足かせになってくる。

ここで、私が使っている Surface Laptop 3 の例を出そう。デバイスマネージャーを開いて、下図のように「表示」→「リソース (種類別)(Y)」を押して、「メモリ」の部分を展開すると MMIO で割り当てられているリソースの一覧が見られる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306456/c4d2a432-a0bf-5256-004e-9e5cbbb539f7.png)

下図が電源ケーブル以外何も接続していない、ほぼ素の状態の Surface Laptop 3 における結果だ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306456/6b137d87-171b-ba3d-7464-8c05aab7ea36.png)

デバイスではなく、ブリッジ(スイッチ)の類もあるため、重複はあるのだが、かなり多くのデバイスがつながっているなぁ、という印象があるのではないだろうか？さらに当然 4GiB のメモリ空間は MMIO 用に全部使えるわけはなく、あくまでもその一部がMMIO用という話になる。Base Addressのアライメント要件もあるために、とにかく bellow 4GiB のメモリ空間はかつかつなのだ。

## 64-bit への移行

というわけで、 MMIO を 4GiB を超える領域にマッピングすれば、この空間かつかつ問題は解決するのだが、当然 32-bit の時代の機器ではできない。多くの要素があるが、ざっくり 64-bit に移行したコンピューター本体(CPU、チップセットやファームウェアなどなど)と 64-bit OS が必要である。その上で、PCIデバイス側が 64-bit 対応となって、先の MMIO セットアップの 「4. UEFIファームウェアがenumerationしたBARに対してメモリアドレスを割り当てる。」 において、 32-bit を超える、すなわち 4GiB、 0xFFFFFFFF を超える領域を割り当てられることになる。

この 4G を超えた領域へのMMIO領域の割り当ての有無を制御するのがBIOS/UEFIの設定画面における「**Above 4G Decoding**」なのである。

# 「Above 4G Decoding を無効化しないと動かないPCI(e)デバイス」とは

…さて、冒頭の記述を思い出してもらおう。

> 「Above 4G Decoding を無効化しないと動かないPCI(e)デバイス」というのが世の中には存在する。
> (略)
> 私が知っている範囲ではこの事象はドライバが原因で起きており、究極的にはそれを改良すれば問題は解決する。

そう、今回の話の肝はドライバである。(今回は話題の関係からKernel-Mode Driverを前提とする。)たしかにPCIデバイスの認識とMMIO領域の割り当てはファームウェアやOSが行う。だが、 **確保した領域への読み書きを担うのはドライバだ** 。64-bitに移行するにはドライバの対応も当然欠かせない。いや、というか、そもそもドライバのバイナリ自体はOSとbitをあわせないと、私が知るメジャーなOSではドライバは動かない。だがしかし、現実には 32-bit を超える領域へのMMIOに対応していない 64-bit OS向けドライバが世の中には存在する。

前述の通りPCIデバイスは自身のBAR Regionが 32-bit か 64-bit なのかを申告することができる。なので、 32-bit にしか対応してなくても、Above 4G Decodingが有効な環境で適切に動作する。もちろん、この申告が間違っているために、表題の「Above 4G Decoding を無効化しないと動かないPCI(e)デバイス」となる、というのは理論的にはありえなくはない。だが、巷で買うことができるPCI関係のIPはたいていまともだし(例外はある)、そもそもビット数指定を間違えると BAR の走査がずれてめちゃくちゃになってまともに動かない(はず)。

だが、ドライバが MMIO 領域の 64-bit 対応を申告する仕組みはない。 MMIO 領域の割当は、ドライバを取り扱うOSの起動前にファームウェアによって行われるからだ。

…いや、事実ではあるのだが、この説明は根っこがおかしい。そもそも **PCIデバイス向けのドライバというのはそのPCIデバイスを作った者によって作られる** のだ。デバイスが 64-bit 対応してて、ドライバが MMIO 64-bit 対応をしていない、というのが根本的におかしい。だが、現実に存在する。うむ、だからこそ「BIOS/UEFIで所定の機能を無効化すると動くが、有効だと動かない」などというトンチキな状況が生まれてしまうわけだ。苦しい。

# IOMMU を用いた仮想マシンへの PCI パススルー

少し話が飛んで、表題の要件たる QEMU 、すなわち仮想マシンの話に移る。

今回自分はとある事情で 「Above 4G Decoding を無効化しないと動かないPCI(e)デバイス」 を PCI パススルーで QEMU 上で使いたいという前提がある。それも、 UEFI on QEMU で。(実は on QEMU で BIOS 使えばこんな問題生じないんじゃね？とちょっと思ったが、もう遅い。そもそもそれできるっけ？教えてエロい人。)

PCI パススルーで仮想マシンにPCIデバイスを渡すと、下に再掲したMMIOに関係したステップがほぼすべてまるまる仮想マシンに移る。「パソコン起動」が「仮想マシン起動」に変わる程度だ。

***
### ***再掲***

1. パソコン起動
2. UEFIファームウェア読み込み、起動
3. UEFIファームウェアがPCI(e)バスを走査してPCI(e)デバイスやPCIブリッジ、PCI(e)スイッチなどをenumeration(列挙)する。この際にBAR(後述)の情報を集める。
4. UEFIファームウェアがenumerationしたBARに対してメモリアドレスを割り当てる。
5. UEFIファームウェアが割り当てたメモリアドレスをBARに書き戻す。
6. OSが起動する(この時ACPI MCFGを介してMMIOのメモリ割り当てを引き渡す)
7. OS + Driver が割り当てられたアドレスに対して読み書きをする、とHW側でマッピングしてくれる。
***

つまり、 **物理マシンにあるファームウェア (BIOS/UEFI) における 「Above 4G Decoding」 の設定は無意味** になる。なぜなら、その物理マシンのファームウェアは該当デバイスの MMIO 領域の割当に関わらないからだ。今回のシチュエーションでは仮想マシンで動作する UEFI ファームウェアがその挙動を支配する。

## 環境の前提

というわけで、ようやく具体的な環境をベースにした話に移っていく。使うディストリビューションは Red Hat Enterprise Linux 8.6 (Ootpa) 、 アーキテクチャは x86_64 だ。

本筋ではないが、一応ハードウェアの基本情報も書いておく。IOMMUをするため、Intel系機材だ。実は [最近知った](https://twitter.com/mine02c4/status/1523655184454275073) のだが、コンシューマー向けAMD機材だとIOMMUグループがざっくばらんすぎて、PCIデバイスをいい感じにパススルーすることが正攻法では難しい。マシン選定の際にはよく検証を。 [ArchWiki](https://wiki.archlinux.jp/index.php/OVMF_%E3%81%AB%E3%82%88%E3%82%8B_PCI_%E3%83%91%E3%82%B9%E3%82%B9%E3%83%AB%E3%83%BC) が大変参考になる。

- CPU : Intel Core i5-9400
- Chipset : Intel H310
- 他、よしなに(書くのがめんどくさくなった)

## 仮想マシンの基本設定

さて、今回用いる RHEL には優秀な公式ドキュメント、しかも日本語がある。

https://access.redhat.com/documentation/ja-jp/red_hat_enterprise_linux/8/html/configuring_and_managing_virtualization/index

これを読んでまずは仮想マシンをPCIパススルーとともにセットアップします。…と言うのはちょっと雑すぎるので、UEFIに絡む領域のみ軽く説明する。OSのインストールの話など一般事項は省く。(なお、もう正確な再現手順は忘れたので、かなり怪しい)

まだOSインストール前なら RHEL のインストール時に仮想マシンの機能を有効に、インストール後なら `dnf` で `virt` モジュールをインストールします。

https://access.redhat.com/documentation/ja-jp/red_hat_enterprise_linux/8/html/configuring_and_managing_virtualization/getting-started-with-virtualization-in-rhel-8_configuring-and-managing-virtualization

ちなみに、必須というわけではないが、下記ドキュメントに従って Cockpit による仮想マシン管理を設定しておくと、何かと便利である。ただし、今回はファームウェア入れ替えなど細かい作業があるので、CLIでの作業を前提とはする。

https://access.redhat.com/documentation/ja-jp/red_hat_enterprise_linux/8/html/configuring_and_managing_virtualization/setting-up-the-rhel-web-console-to-manage-vms_managing-virtual-machines-in-the-web-console

仮想マシンの作成に関するドキュメントは下記である。

https://access.redhat.com/documentation/ja-jp/red_hat_enterprise_linux/8/html/configuring_and_managing_virtualization/assembly_creating-virtual-machines_virt-getting-started

今回は仮想マシンを以下のように作る。重要なのは `--boot` オプションである。ほかは各々の状況に合わせてほしい。

```sh
virt-install --name demo-guest1 --boot uefi --memory 2048 --vcpus 2 --disk size=80 --os-variant win10 --cdrom /home/username/Downloads/Win10install.iso --video qxl  --noreboot --noautoconsole
```

`--boot uefi` をつけてセットアップした仮想マシンには domain の XML 定義に OVMF を loader として指定している部分がある。 `virsh dumpxml demo-guest1` などのように表示すると `/domain/os/loader` (in XPath) のノードが下記のようになっているはずである。

```XML
<loader readonly='yes' secure='yes' type='pflash'>/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd</loader>
```

まさにここに指定されている `/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd` がUEFIファームウェアである。結果だけ言えば

https://github.com/Mine02C4/edk2/releases/tag/ovmf-disable-above4G-for-bus-0x10-20220516

から `OVMF_DISABLE_ABOVE_4G_FOR_BUS_0x10.fd` をダウンロードして、 `/usr/share/edk2/ovmf/custom` など好きな場所に置いて、権限やSELinuxラベルなどを調整して、 `virsh edit demo-guest1` で先の loader ノードの中身を新しいファームウェアの絶対パスに変える。その後に、何らかの手段で「シャットダウン」→「起動」をする。 **ゲストOSの再起動では新しいファームウェアは読み込まれない** ので要注意だ。

するとあら不思議！ PCIバス 0x10 に接続されたデバイスが無条件で 32-bit アドレス範囲にマッピングされます！

# OVMFのカスタマイズ

**「おいふざけんな、ここまで説明しておいて、ソースコードとバイナリだけリンク貼って終わりはねえだろ」**

という声が聞こえてきそうなので、何をどうカスタマイズしたか説明する。

## EDK II のセットアップ

最初のステップはファームウェア開発環境である [EDK II](https://github.com/tianocore/edk2) をセットアップだ。

まずは爆速なプロセッサとLinux環境(もしくはWindows環境)を用意する。あとは大容量メモリと爆速なネット回線があると望ましい。(kernelじゃないのでいうほど重くはないです) 見ての通り EDK II は VisualStudio でもビルドできるのだが、当方は Linux で作業した。Dockerでガシガシやりたかったので。

https://github.com/tianocore/tianocore.github.io/wiki/Getting-Started-with-EDK-II

究極的には上の公式 Wiki の記事に従うことになるが、自分はこの環境を Docker を利用して構築した。

https://github.com/Mine02C4/edk2_docker_environment

(自分用だが)サクッと使えるだろうDockerのスクリプトを上のリポジトリに用意した。 2022/05/16 時点では動いた。

READMEの通りだが clone して `make enter` して、あとは EDK II のドキュメント通りの作業を行うことができる。


なお、コンテナ内での git clone に関して、今回の記事では https://github.com/tianocore/edk2/releases/tag/edk2-stable202202 をもとに変更を行ったので、私自身の作業をトレースしたいのであれば、ここを起点にブランチを作るとよい。直接私のカスタムを clone する場合は

```sh
git clone https://github.com/Mine02C4/edk2.git -b ovmf-disable-above4G-for-bus-0x10-20220516
```

といった感じになるだろう。

その後 submodule update した後に基本的なツールをビルドし読み込む。これで EDK II 自体は準備完了だ。

## OVMF 向け target.txt

EDK II で OVMF をビルドするには ./Conf/target.txt にそのオプションを指定することになる。私の変更は大きくないので `edk2-stable202202` とほぼ同じくどの variant も動くとは思うが、直接確認したのは以下の設定である。(target.txtの変更行のみをピックアップ)

```
ACTIVE_PLATFORM       = OvmfPkg/OvmfPkgX64.dsc
TARGET                = RELEASE
TARGET_ARCH           = X64
TOOL_CHAIN_TAG        = GCC5
```

どの variant も、と言ったが TOOL_CHAIN_TAG はコンテナ側で決まるので `GCC5` 固定になるだろう。

## ビルド

差分の説明をする前にビルド方法。と言っても edk2 をクローンしたディレクトリで

```
build
```

とするだけである。ヘルプを見るとわかるが、GNU Makeとは異なり、並列化オプションをつけなくても、いい感じに並列コンパイルしてくれる。後は応援。

## 差分

さて、くどいが MMIO 初期化ステップを再掲する。

***
### ***再掲***

1. パソコン起動
2. UEFIファームウェア読み込み、起動
3. UEFIファームウェアがPCI(e)バスを走査してPCI(e)デバイスやPCIブリッジ、PCI(e)スイッチなどをenumeration(列挙)する。この際にBAR(後述)の情報を集める。
4. UEFIファームウェアがenumerationしたBARに対してメモリアドレスを割り当てる。
5. UEFIファームウェアが割り当てたメモリアドレスをBARに書き戻す。
6. OSが起動する(この時ACPI MCFGを介してMMIOのメモリ割り当てを引き渡す)
7. OS + Driver が割り当てられたアドレスに対して読み書きをする、とHW側でマッピングしてくれる。
***

今回の目標は 64-bit BAR 対応を申告してきた PCI デバイスに対して、今は above 32-bit の領域を割り当てているが、 32-bit 以内の領域を割り当てる様に動作を変更することだ。先の手順で 3. か 4. に変更を加えればよいということになるが、私は楽そうな 3. に手を入れることにした。すなわち、メモリアドレス割り当てのロジックにデバイス情報を渡す時点で、 64-bit 対応デバイスを 64-bit 非対応だと嘘の情報に書き換える方法だ。

https://github.com/Mine02C4/edk2/commit/efc071592dc06f6214ec54c3910297afab50d1e1

というわけで、これがその差分である。そこ！「ここまで御託並べておいて、たったの 37行追加 かよ！」とか言わない！

OVMF における PCI enumeration は `MdeModulePkg/Bus/Pci/PciBusDxe/` 以下の `PciEnumerator.c` の `PciEnumerator` 関数を中心に `PciEnumeratorSupport.c` など複数のファイルにまたがって実装されている。私はこのうち BAR の情報を取得し解釈する `PciParseBar` 関数に手を入れた。 `ForceDisableAbove4G` というフラグを用意し、関数冒頭で「**特定の条件**」で `TRUE` とすることで、それを強制的に 32-bit のデバイスとして認識させている。

さて、「**特定の条件**」が問題であるが、単に「Above 4G Decoding」の無効化を再現するなら無条件にすればよい。ただ、それだと大きな MMIO 領域を適切に使える他のデバイスが活躍できないし、デバイスを増やしすぎると起動しなくなる。

**せっかくファームウェアを掌握できる仮想マシンなのだ！**

というわけで、自由に改造しちゃいましょう。私は PCI Bus 0x10 に繋がったデバイスのみで強制的な 32-bit アドレスの使用を行うことにした。数字は適当である。が、 PCI Bus を判断基準にしたのは libvirt Domain XML での PCI デバイス設定で融通が利くパラメータだからだ。

今回の改造には設定画面などを一切作りこんでいない。めんどくさいから。もしPCIデバイスの Vendor ID などを判断基準にしてしまうと、この機能の対象にする機材を変更するたびに OVMF をリビルドしないといけない。それはさすがにめんどくさすぎる。一方で実機と異なり仮想マシンの場合は PCI Bus の番号を自由につけられる。いくらでもPCI Expressスイッチも作り放題だ！(上限はあるけど)というわけで、小さい番号の Bus は virtio などで予約済みだったりするので、 0x10 を今回用いた。

libvirt では `virsh edit <domain>` で Domain XML を編集できる。そこで、ホストのPCI デバイスをパススルーには下記の `hostdev` 要素を `/domain/devices/hostdev` となるように入れ込むことになる。

```xml
    <hostdev mode='subsystem' type='pci' managed='yes'>
      <driver name='vfio'/>
      <source>
        <address domain='0x0000' bus='0x02' slot='0x00' function='0x0'/>
      </source>
      <address type='pci' domain='0x0000' bus='0x10' slot='0x00' function='0x0'/>
    </hostdev>
```

この内 `//hostdev/address/@bus` の部分が私の差分で確認している部分になる。ホスト側からときに見た bus の番号(`//hostdev/source/address/@bus`に相当)とは無関係に、ここの値だけで制御できるのが一番便利だと思った次第だ。

…いや、まあ「Above 4G Decoding を無効化しないと動かないPCI(e)デバイス」がそう何個もあってたまるか！という感じではあるが。

# 実際にどうなるか

というわけで、試して見た結果を書こう。まずはホストの環境で該当デバイスがどう認識されているかを念のため確認する。なお、ホストのファームウェアでは Above 4G Decoding を有効にしている。さくっと `lspci` で確認する。なお、結果は一部略している。

```sh
$ sudo lspci -vv -s 02:00.0
02:00.0 Multimedia controller: (略)
        Subsystem: (略)
        Control: (略)
        Status: (略)
        Latency: (略)
        Interrupt: (略)
        IOMMU group: 10
        Region 0: Memory at 2fffe01000 (64-bit, prefetchable) [size=4K]
        Region 2: Memory at 2fffe00000 (64-bit, prefetchable) [size=4K]
(以下略)
```

前述の MMIO 割当領域というのは Region 0, 2 に相当する。見ての通り 64-bit, prefetchable (OVMFのソースコードでは PMem64) と判断されており、ファームウェアの設定通り割当は 4G の領域を超えている。

さて、この状況でまずはデフォルトの OVMF ファームウェアを用いた仮想マシンを起動してみよう。なお、諸事情でゲストOSはWindowsである。ちょうどOS毎のBAR情報の読み方も紹介できてちょうどよい(?)。この際の loader は以下の通りである。

```XML
<loader readonly='yes' secure='yes' type='pflash'>/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd</loader>
```

この状態でデバイスマネージャーで該当デバイスのプロパティを開いて「リソース」タブを開くと、MMIO割当領域が見える。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306456/aca19262-dc7a-26a1-49ee-aef898c64017.png)

こちらも、 32-bit 領域を超えており、 Above 4G に割り当てられている。そして、ドライバはバグっているわけである()
なお、同じ Above 4G でもホストでの割当とぜんぜん違うことがわかるだろう。前述の通り、ゲストはホストとは独立して MMIO 領域を割り当てているからである。なお、この結果はホストでの Above 4G Decoding を無効化しても同じである。問答無用で Above 4G に割り当ててくる。

これは、ちょっと前に Surface Laptop 3 の例で出した、デバイスマネージャの「表示」→「リソース (種類別)(Y)」からも見ることができる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306456/a3771ee6-a4df-ed71-e89f-82634b6cebcb.png)

赤で伏せた部分がちょうど該当デバイスとなっている。なお、前後を見ると VirtIO 系の仮想デバイスは概ね 64-bit 対応していることが見て取れる。

さて、では仮想マシンを止めて `virsh edit` で loader を

```XML
<loader readonly='yes' secure='yes' type='pflash'>/usr/share/edk2/ovmf/custom/OVMF_DISABLE_ABOVE_4G_FOR_BUS_0x10.fd</loader>
```

のように、今回のカスタムのものに変えよう。まずは、デバイスのプロパティを確認する。今回は 0x10 、すなわち 16 番につなげているものを、強制的に 32-bit にする仕様なので、まずはバス番号を確認する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306456/1fd18a81-5589-4cee-aff6-ae2296808301.png)

16 になっている。そして、リソースタブを見ると…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306456/88e724a9-6b2d-f0de-6142-d36e867d4fe8.png)

**YES!** 0xC9401000 、 0xC9400000 、 ともに 32-bit に収まっている！そして、先と同様にリソース一覧を見よう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306456/a13bb08b-fc5c-17f3-3984-5462e96610ee.png)

今度も赤く塗った部分が該当デバイスである。先程 64-bit 領域にマップされていたものは引き続き 64-bit に、でも該当デバイスだけ 32-bit 領域に動いている。やったぜ！！！

# おわりに

実機の UEFI ファームウェア をいじって細かい MMIO 制御をすることは至難の業だが、仮想マシンであればファームウェアもカスタムし放題だ！まあカスタムして何をやるか…ってのはあるのだが、実機ではうまいこといかないことも、無理くり QEMU + OVMF 、そして IOMMU を駆使して対応が可能だ。