[Qemu] エミュレートされたNVMeドライブをWindowsゲストに接続する

# はじめに
　WindowsがNVMeドライブをどのように操作しているのかを深く知るには、WindowsがNVMeドライブに対して、どのようなコマンドを発行し、どのようにレジスタアクセスしているのか、を知る必要があります。

　知りたいことに合わせたNVMeドライブ（実機）を用意するのはコストがかかりますので、こうゆう時は積極的に仮想マシンを使うべきです。

　ということで、Qemu<sup>[1]</sup>を使用して、WindowsゲストにエミュレートされたNVMeドライブを接続する方法を、備忘録代わりに記します。

　※ 本当は、BIOSによるアクセスとOS (Windows)によるアクセスを分けて考える必要がありますが、ここではまとめて扱います。

# やりたいこととやったこと
* **やりたいこと：**
    * WindowsがNVMeドライブにどんなアクセスをするのか（特に起動時）を、NVMeドライブの実機なしで明らかにしたい
* **やったこと：**
    * Qemu（ホストOS：Linux、ゲストOS：Windows）を使い、QemuによるNVMeドライブエミュレーションコードを改造して、WindowsによるNVMeドライブへのアクセスの内容をコンソール出力させた 

# 準備
## 今回用意したもの

* Linuxホスト
    * x86_64ホスト＋Ubuntu 18.04.4 LTS
* Qemuソース
    * バージョン4.1.0
* Windows ISOイメージ
 * Windows Insider Program<sup>[2]</sup>で取得したWindows 10 Insider Preview build 19041.84のISOイメージ

## ここで仮定するディレクトリ構成

```
qemu --- qemu-4.1.0 ：qemu-4.1.0のソースディレクトリ
      |- win        ：ISOイメージを置いたり、ドライブイメージファイルを置くディレクトリ 
```

# 手順
## Qemuビルド
　ホストが`x86_64`ですので、`configure`スクリプトの実行時に`--target-list=x86_64-softmmu`オプションをつけて、必要なバイナリのみビルドします。時間とスペースの節約になります。

```
% tar Jxvf qemu-4.1.0.tar.xz
% cd qemu-4.1.0
% ./configure --target-list=x86_64-softmmu
% make
```

　ビルドの結果、`qemu-4.1.0/x86_64-softmmu`ディレクトリが作成されます。

## ドライブイメージファイル作成
　WindowsをインストールするドライブとNVMeドライブの2つを作成します。

　Windowsをインストールするドライブのサイズが小さすぎるとWindowsがインストールできないので注意してください。以下の例では40 GBにしています。NVMeドライブのサイズは、必要がなければ小さくてもOKです。

```
% cd ../win
% ../qemu-4.1.0/qemu-img create -f qcow2 qemu-win.qcow2 40G
% ../qemu-4.1.0/qemu-img create -f qcow2 qemu-nvme.qcow2 32G
```

　`qemu-win.qcow2`がWindowsをインストールするドライブのイメージファイル、`qemu-nvme.qcow2`がNVMeドライブのイメージファイルです。ファイル名は任意です。

## Qemu起動用スクリプト作成
　Qemuの起動コマンドを毎回打つのは面倒なので、シェルスクリプトを作成します。

　作成するスクリプトは2つです。ひとつはWindowsのISOイメージから起動してWindowsをインストールするためのスクリプトで、もうひとつは、NVMeドライブを接続した状態で、WindowsをインストールしたドライブからWindowsを起動するためのスクリプトです。

　一つ目のスクリプトはこんな感じです。色々と改良したい点はあるのですが、とりあえず現状をそのままで……

```bash:install-win10-on-qemu.sh
#!/bin/bash

../qemu-4.1.0/x86_64-softmmu/qemu-system-x86_64 \
 -enable-kvm \
 -boot d \
 -cdrom Windows10_InsiderPreview_Client_x64_en-us_19041.iso \
 -hda qemu-win.qcow2 \
 -m 2048
```

　二つ目のスクリプトはこんな感じです。

```bash:launch-qemu-with-nvme-drive.sh
#!/bin/sh

../qemu-4.1.0/x86_64-softmmu/qemu-system-x86_64 \
 -enable-kvm \
 -boot c \
 -hda qemu-win.qcow2 \
 -m 2048 \
 -device nvme,drive=nvme0,serial=xxxx \
 -drive file=qemu-nvme.qcow2,if=none,id=nvme0 \
 -monitor stdio
```

　シリアル番号の部分（`serial=xxxx`の`xxxx`の部分）は何でも構いません。ちなみにここで設定したシリアル番号は、Crystal Disk Infoなどで表示されます。また、どちらのスクリプトでも、仮想マシンに割り当てるメモリサイズ（`-m`オプションの値）などはホストシステムの環境に合わせて設定してください。

　`-monitor stdio`というオプションは、Qemuのモニタ（ディスプレイモニタのことではない）出力を標準出力に設定するオプションです。後ほどモニタ経由でコンソールに色々出力させるので、そのために指定しています。

## Windowsインストール
　先ほど作成した一つ目のスクリプト(`install-win10-on-qemu.sh`)でQemuを起動します。

　その後、別のターミナルでVNCビューワーを立ち上げて、Qemuが出力している仮想ディスプレイに接続します。例えば`% gvncviewer localhost:0`みたいな感じです。

　すると、以下のようにWindowsのインストールが始まっているのが見られます。
![Windowsのインストール開始画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/439683/29bde4f3-01f4-ce1d-fe57-d21a4936f52c.png "Windowsのインストール開始画面")
図1：Windowsのインストール開始画面

　Windowsのインストール手順自体は通常のインストール手順と何も変わりません。インストールが終わったら一旦Windowsをシャットダウンします（Qemuも終了する）。

## NVMeドライブを接続してWindowsを起動
　作成した二つ目のスクリプト(`launch-qemu-with-nvme-drive.sh`)を使用し、エミュレートされたNVMeドライブを接続した状態で、先ほどインストールしたWindowsの仮想マシンを起動します。VNCビューワーでQemuの出力先仮想ディスプレイに接続する必要があるのは、Windowsインストール時と同じです。

　もし`qemu`実行時に`/dev/kvm`へのアクセスについて`Permission Denied`と怒られる場合は、スクリプト内で`sudo`をつけて`qemu`を実行する、もしくは`qemu-kvm`をインストールして、使用しているユーザをグループ`kvm`に追加すれば実行できます。

　QemuがエミュレートしたNVMeドライブが正常に認識されているかどうかは、起動したWindowsの「デバイスマネージャ」や「ディスクの管理」メニューで確認できます。以下は、「ディスクの管理」メニューでと「デバイスマネージャ」で確認した結果です。
![エミュレートされたNVMeドライブを接続した状態のQemu上のWindows仮想マシン](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/439683/3ed60a24-a4d1-b4d1-a78d-7cd575c03e2a.png "NVMeドライブを接続した状態のQemu上のWindows仮想マシン")
図2：エミュレートされたNVMeドライブを接続した状態のQemu上のWindows仮想マシン

# QemuのNVMeエミュレーションを弄ってみる
　これで、Windowsに、エミュレーションしたNVMeドライブを接続できるようになったので、Qemuのソースを弄ればWindowsの挙動を色々と知ることができます。

　ここでは、手始めに、Windowsが起動時にNVMeドライブに対してどんなアクセスを行うのか調べてみます。

## ソースファイルの改造内容
　QemuにおけるNVMeドライブのエミュレーションは`hw/block/nvme.c`で行われています。

　例えば、PCIeデバイス、およびNVMeドライブとしての初期化は`nvme_realize()`で行われています。前半はPCIeデバイスとしての設定、後半はNVMeドライブとしての設定（`Identify`コマンドで返す内容や設定レジスタの内容を含む）に相当します。 

```c:hw/block/nvme.c
static void nvme_realize(PCIDevice *pci_dev, Error **errp)
{
    NvmeCtrl *n = NVME(pci_dev);
    NvmeIdCtrl *id = &n->id_ctrl;
    ...(snip)...
    pci_conf = pci_dev->config;
    pci_conf[PCI_INTERRUPT_PIN] = 1;
    pci_config_set_prog_interface(pci_dev->config, 0x2);
    pci_config_set_class(pci_dev->config, PCI_CLASS_STORAGE_EXPRESS);
    pcie_endpoint_cap_init(pci_dev, 0x80);
    ...(snip)...
    n->num_namespaces = 1;
    n->reg_size = pow2ceil(0x1004 + 2 * (n->num_queues + 1) * 4);
    n->ns_size = bs_size / (uint64_t)n->num_namespaces;

    n->namespaces = g_new0(NvmeNamespace, n->num_namespaces);
    n->sq = g_new0(NvmeSQueue *, n->num_queues);
    n->cq = g_new0(NvmeCQueue *, n->num_queues);

    memory_region_init_io(&n->iomem, OBJECT(n), &nvme_mmio_ops, n,
                          "nvme", n->reg_size);
    pci_register_bar(pci_dev, 0,
        PCI_BASE_ADDRESS_SPACE_MEMORY | PCI_BASE_ADDRESS_MEM_TYPE_64,
        &n->iomem);
    msix_init_exclusive_bar(pci_dev, n->num_queues, 4, NULL);

    id->vid = cpu_to_le16(pci_get_word(pci_conf + PCI_VENDOR_ID));
    id->ssvid = cpu_to_le16(pci_get_word(pci_conf + PCI_SUBSYSTEM_VENDOR_ID));
    ...(snip)...
}
```

　また、設定レジスタの読み書きは、それぞれ`nvme_mmio_write()`と`nvme_mmio_read()`で実装されています。
　`nvme_mmio_read()`の実装は`memcpy()`するだけなのでちょっと面白くないのですが、`nvme_mmio_write()`は`nvme_write_bar()`を呼んで、その中で、NVMe仕様に基づいて書き込まれた値の解釈とドライブ機能の設定などを行っています。

　以下は、`nvme_write_bar()`の内容から、CC (Controller Configuration)レジスタへの書き込みを処理している部分を抜粋したものです。

```c:hw/block/nvme.c
static void nvme_write_bar(NvmeCtrl *n, hwaddr offset, uint64_t data, unsigned size)
{
    ...(snip)...
    switch (offset) {
    ...(snip)...
    case 0x14:  /* CC */
        trace_nvme_mmio_cfg(data & 0xffffffff);
        qemu_printf( "[NVME] ==> CC "); ★
        qemu_printf( "< EN(%ld), CSS(0x%lX), MPS(0x%ld), AMS(0x%lX), SHN(0x%lX) IOSQES(%ld) IOCQES(%ld) >\n", data&0x1, (data>>4)&0x7, (data>>7)&0x4, (data>>11)&0x7, (data>>14)&0x3, (data>>16)&0xF, (data>>20)&0xF); ★
        /* Windows first sends data, then sends enable bit */
        if (!NVME_CC_EN(data) && !NVME_CC_EN(n->bar.cc) &&
            !NVME_CC_SHN(data) && !NVME_CC_SHN(n->bar.cc))
        {
            n->bar.cc = data;
        }

        if (NVME_CC_EN(data) && !NVME_CC_EN(n->bar.cc)) {
            n->bar.cc = data;
            if (unlikely(nvme_start_ctrl(n))) {
                trace_nvme_err_startfail();
                n->bar.csts = NVME_CSTS_FAILED;
            } else {
                trace_nvme_mmio_start_success();
                n->bar.csts = NVME_CSTS_READY;
            }
        } else if (!NVME_CC_EN(data) && NVME_CC_EN(n->bar.cc)) {
            trace_nvme_mmio_stopped();
            nvme_clear_ctrl(n);
            n->bar.csts &= ~NVME_CSTS_READY;
        }
        if (NVME_CC_SHN(data) && !(NVME_CC_SHN(n->bar.cc))) {
            trace_nvme_mmio_shutdown_set();
            nvme_clear_ctrl(n);
            n->bar.cc = data;
            n->bar.csts |= NVME_CSTS_SHST_COMPLETE;
        } else if (!NVME_CC_SHN(data) && NVME_CC_SHN(n->bar.cc)) {
            trace_nvme_mmio_shutdown_cleared();
            n->bar.csts &= ~NVME_CSTS_SHST_COMPLETE;
            n->bar.cc = data;
        }
        break;
    ...(snip)...
    }
}
```

　行末に「★」をつけた行は私が追加したものです。このいわゆるデバッグプリント文の出力内容は、Qemuのモニタに出力されます。起動スクリプト(`launch-qemu-with-nvme-drive.sh`)で`-monitor stdio`というオプションを付けましたので、この`qemu_printf()`による出力は標準出力に出力されます。

　実際に色々出力を追加したQemuを実行して、ゲストOSのWindowsにエミュレートされたNVMeドライブを接続すると、以下のスクリーンショットのように、Qemuのモニタに、追加したデバッグ出力が表示されます（右側のターミナル）。
![NVMeドライブのエミュレーションコードにデバッグプリントを追加したQemuの実行の様子](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/439683/44099003-5689-fd0d-8670-73ce4b594341.png)
図3：NVMeドライブのエミュレーションコードにデバッグプリントを追加したQemuの実行の様子

## Qemuで捉えたWindowsによるNVMeドライブ接続時の処理
　図3の右側のウィンドウ（コンソール）に出力されているメッセージは、Windows起動時のNVMeドライブに対するアクセスの内容です。せっかくですので、この内容を順に追ってみます。

```
[NVME] <== VS < MJR(1), MNR(2), TER(0) >
```
　NVMeのバージョンを確認するために、Version (VS)レジスタを読み出しています。"1.2"相当の値をホストに返しています。

```
[NVME] <== CAP < MQES(255), CQR(1), AMS(0x0), TO(15), DSTRD(0), NSSRS(0), CSS(0x00), BPS(0), MPSMIN(0), MPSMAX(0) >
```
　NVMeドライブが備える機能や設定を確認するために、Controller Capabilities (CAP)レジスタを読み出しています。

```
[NVME] ==> CC < EN(0), CSS(0x0), MPS(0x0), AMS(0x0), SHN(0x0) IOSQES(0) IOCQES(0) >
```
　ここでController Configuration (CC)レジスタへ書き込んでいます。

　NVMe仕様<sup>[3]</sup>では、CCレジスタのEnable (EN)ビットに0が書き込まれると、コントローラは、自身が再有効化(re-enable)可能になった時点でController Status (CSTS)レジスタのReady (RDY)ビットを0にすること、と定義されています。この仕様に基づいた書き込みだと思われます。

```
[NVME] <== CAP < MQES(255), CQR(1), AMS(0x0), TO(15), DSTRD(0), NSSRS(0), CSS(0x00), BPS(0), MPSMIN(0), MPSMAX(0) >
[NVME] <== CSTS < RDY(0), CFS(0), SHST(0x0), NSSRO(0), PP(0) >
```
　CCレジスタのENビットに0を書き込んだのち、CAPレジスタを読んだ後で、CSTSレジスタを読んでいます。ここでコントローラがRDYビットが0のデータを返すので、ホストはコントローラを有効にする準備ができたことを知ります。

```
[NVME] <== CAP < MQES(255), CQR(1), AMS(0x0), TO(15), DSTRD(0), NSSRS(0), CSS(0x00), BPS(0), MPSMIN(0), MPSMAX(0) >
[NVME] ==> CC < EN(1), CSS(0x0), MPS(0x0), AMS(0x0), SHN(0x0) IOSQES(6) IOCQES(4) >
```
　CSTSレジスタのRDYビットが0であることを知ったホストは、CCレジスタのENビットに1を書き込みます。これによって、NVMeドライブが（正確にはコントローラが）有効になり、Adminコマンドを受け付けられるようになります。

```
[NVME] <== CAP < MQES(255), CQR(1), AMS(0x0), TO(15), DSTRD(0), NSSRS(0), CSS(0x00), BPS(0), MPSMIN(0), MPSMAX(0) >
[NVME] <== CSTS < RDY(1), CFS(0), SHST(0x0), NSSRO(0), PP(0) >
[NVME] ==> CMD: Identify < CNS(1), CNTID(0), NSID(0) >
```
　ホストはさっそくコントローラを指定した`Identify`コマンドを投げてきます。「コントローラを指定している」ことは、`Identify`コマンドのCNSフィールドが1であることからわかります。

　この時、`Identify`コマンドはAdminコマンド用のSubmission Queueに投入されます。したがって、本当はコマンドを投入する前にAdminコマンド用のSubmission QueueとCompletion Queueのアドレスを取得するためにレジスタ（Admin Submission Queue Base (ASQB)レジスタとAdmin Completion Queue Base (ACQB)レジスタ）を読み出すはずなのですが...

```
[NVME] <== CAP < MQES(255), CQR(1), AMS(0x0), TO(15), DSTRD(0), NSSRS(0), CSS(0x00), BPS(0), MPSMIN(0), MPSMAX(0) >
[NVME] ==> CMD: Create IO Completion Queue: < QSIZE(255), QID(1), PC(0x1), IEN(0x0), IV(0) >
[NVME] <== CAP < MQES(255), CQR(1), AMS(0x0), TO(15), DSTRD(0), NSSRS(0), CSS(0x00), BPS(0), MPSMIN(0), MPSMAX(0) >
[NVME] ==> CMD: Create IO Submission Queue: < QSIZE(255), QID(1), CQID(1), PC(0x1), QPRIO(0x0) >
```
　ホストは、その後、`Create IO Submission Queue`コマンドと`Create IO Completion Queue`コマンドで、Adminコマンド以外のコマンド用のSubmission QueueとCompletion Queueを作成します。

　これでやっとReadコマンドやWriteコマンドを発行できるようになります。

```
[NVME] ==> CMD: Identify < CNS(0), CNTID(0), NSID(1) >
```
　ホストは、今度はNSIDが1のネームスペースに対して`Identify`コマンドを投げてきます。`Identify`コマンドの対象がネームスペースであることは、コマンドのCNSフィールドが0であることからわかります。

　この後ReadコマンドやWriteコマンドが発行されるはずですが、現時点ではReadコマンドやWriteコマンドの処理部分にデバッグプリントを仕込んでいませんので、ReadやWriteの様子を確認することはできていません。

# まとめ
　この記事では、Qemuを使用して、エミュレートされたNVMeドライブをWindowsゲストに接続する方法を、備忘録代わりにまとめました。

　また、QemuのNVMeドライブエミュレーションコードを改造することで、WindowsがNVMeドライブをどのように操作しているのかを調べる方法も示しました。

　現在のQemuのNVMeドライブエミュレーションは、準拠しているNVMeバージョンが1.2でちょっと古いですが（最新仕様は1.4）、基本的な動作の調査・確認であれば十分に利用できると思います。

# References
[1] _QEMU_, https://www.qemu.org/
[2] _Flight Hub - Windows Insider Program_, https://docs.microsoft.com/en-us/windows-insider/flight-hub/
[3] NVM Express, _"NVM ExpressTM Base Specification"_, Revision 1.3d, March 20, 2019

# ライセンス表記
<a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/"><img alt="クリエイティブ・コモンズ・ライセンス" style="border-width:0" src="https://i.creativecommons.org/l/by-sa/4.0/88x31.png" /></a><br />この記事は<a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/">クリエイティブ・コモンズ 表示 - 継承 4.0 国際 ライセンス</a>の下に提供されています。