BitVisorのI/Oフックをバイパスされるケースについて少し紹介します。
例としてI/Oポート0x70にあるRTCのインデックスの書き込みアクセスをフックしてみることにします。
BitVisor側実装例
diff --git a/core/io_iohook.c b/core/io_iohook.c
--- a/core/io_iohook.c
+++ b/core/io_iohook.c
@@ -150,6 +150,25 @@ kbdio_dbg_monitor (enum iotype type, u32
}
#endif
+static enum ioact
+io0x70_monitor (enum iotype type, u32 port, void *data)
+{
+ do_io_default (type, port, data);
+ switch (type) {
+ case IOTYPE_OUTB:
+ printf ("outb $0x%02X, $0x%02X\n", *(u8 *)data, port); break;
+ case IOTYPE_OUTW:
+ printf ("outw $0x%04X, $0x%02X\n", *(u16 *)data, port); break;
+ case IOTYPE_OUTL:
+ printf ("outl $0x%08X, $0x%02X\n", *(u32 *)data, port); break;
+ case IOTYPE_INB:
+ case IOTYPE_INW:
+ case IOTYPE_INL:
+ break;
+ }
+ return IOACT_CONT;
+}
+
static void
setiohooks (void)
{
@@ -165,6 +184,7 @@ setiohooks (void)
if (config.vmm.f11panic || config.vmm.f12msg)
set_iofunc (0x60, kbdio_dbg_monitor);
#endif
+ set_iofunc (0x70, io0x70_monitor);
}
INITFUNC ("pass1", setiohooks);
非常に簡単なもので、I/Oポート0x70への書き込み内容をログに残します。これでGNU/Linuxなどを起動すると、最初から結構大量にログが残っているかと思いますが、気にしないで先に進みます。
フック回避コード実装例
面倒なのでGNU/Linux上のプログラムで直接RTCにアクセスします。他のプログラムのアクセスと競合するとまずいことになるかも知れませんのでご注意ください。
#include <sys/io.h>
#include <stdio.h>
#include <err.h>
int
main (int argc, char **argv)
{
if (iopl (3))
err (1, "iopl");
asm volatile ("cli");
outb (9, 0x70);
int year = inb (0x71);
outb (8, 0x70);
int month = inb (0x71);
outw (0x900, 0x6f);
int year2 = inb (0x71);
asm volatile ("sti");
printf ("Year: %x\n", year);
printf ("Month: %x\n", month);
printf ("Year2: %x\n", year2);
return 0;
}
最初は普通にRTCから年と月を読み出します。それぞれインデックスは9と8です。これはBitVisorのログにも残ります。その後、インデックスを普通じゃない方法で9に変えて年を読み出します。これはBitVisorのログに残りません。
実行例
実行すると以下のように出ます。ちゃんと年が読めているのがわかります。
Year: 18
Month: 12
Year2: 18
しかし、BitVisorのログを調べると、以下のように最後は8の書き込みで止まっているわけです。
outb $0x09, $0x70
outb $0x08, $0x70
理屈
どうしてこうなるかについては、8086時代からx86に慣れ親しんできた皆さんはよくご存知のことと思いますが、簡単に説明してみます。
IBM PCは、8088というCPUを搭載しています。これはデータバスが8ビットです。データバスが8ビットなので、16ビットのアクセスはすべて8ビットずつの2回のアクセスに分割されます。よって、上のようにI/Oポート0x6fに16ビットの0x0900を書き込めば、それは0x6fへの0x00の書き込みと、0x70への0x09の書き込みになります。もっとも、当時はRTCは搭載されていなかったと思いますが... CRTコントローラーへの書き込みなどで、16ビットアクセスを駆使する例はあった... はずです。たぶん。
8086の場合は、データバスが16ビットです。8086では、データバスが16ビットでも、引き続き8ビット単位のアクセスが可能です。偶数アドレスの16ビットアクセスは、1回で行うことができます。奇数アドレスの16ビットアクセスは、8088と同様、2回に分割されます。
相沢 一石 (著) "8086ファミリ・ハンドブック" より引用:
8086はこのようにアドレスとデータを時分割で切り替えて使うようになっていて、アドレスは20ビット、データは16ビットあります。アドレスバスには奇数アドレスも出るようになっていますが、1バイトアクセス時のデータバスは偶数アドレスが下位8ビット、奇数アドレスが上位8ビットを使用します。IBMと異なり最初から8086を採用していたNEC PC-9801で、割り込みコントローラー8259など8ビットデバイスのI/Oアドレスが飛び飛びになっていたのは、それらが片側の8ビットのみに接続されていたためです。1バイトアクセスの識別のためにBHEという信号が使われます。
16ビットのデータバス上で8ビットアクセスが問題となるのは、書き込みのように状態が変化する場合だけなので、ROMを接続する場合は、BHEとアドレスの最下位ビットは無視して、16ビット分のデータバスに接続されるようです。この話は、アドレスバスが20ビット分あるのに奇数アドレスのアクセスを16ビット1回でできるようにしなかったのはなぜだろう、というささやかな疑問を一瞬で解決してくれました。そりゃ、奇数アドレスのアクセスのためだけに、上下8ビットを入れ替えたりアドレスをインクリメントしたりする回路なんて誰も作りたくないですよね。
おそらく16ビットCPUの時代(80386SXまで)はこんなような形だったんだと思いますが、32ビットになるとどうなったかというと、アドレスバスの下位2ビットは使われなくなり、BHEの代わりに、どのバイトをアクセスしようとしているかを示すBE0#からBE3#の4つができたようです。なので、例えばアドレス0x6fに32ビットのアクセスを行えば、アドレス0x6cに対してBE3#だけアクティブなアクセスと、アドレス0x70に対してBE0#, BE1#とBE2#がアクティブなアクセスが行われる、んじゃないかと思います。たぶん。64ビットCPUについては、調べていません。
そんなわけですので、アドレス0x6fの16ビットアクセスは、やはり8ビットずつの2回のアクセスになるわけです。
その他のケース
今回のI/Oポート0x6fの16ビットアクセスは、確実に2回のアクセスになりますが、例えばI/Oポート0x70を16ビットで読み出すと何が出てくるかというと、CPUとしては16ビット1回のアクセスをするものの、互換性のためにこれを周辺装置側で2回分のアクセスとみなすか、片方の8ビットだけのアクセスとみなすか、そんなイレギュラーなアクセスには対応しないかで、実は機種依存になります。
#include <sys/io.h>
#include <stdio.h>
#include <err.h>
int
main (int argc, char **argv)
{
if (iopl (3))
err (1, "iopl");
asm volatile ("cli");
outb (9, 0x70);
int data = inw (0x70);
asm volatile ("sti");
printf ("data: %x\n", data);
return 0;
}
このプログラムを、Ryzen 7 2700 (B350M-A)環境で実行すると、以下のようになります:
data: 2609
26はなんだかわかりませんが、09はそのまま書き込んだ値が読めるようです。全く同じプログラムをThinkPad X201で実行すると、以下のようになります:
data: ffff
ThinkPadは不正な16ビット読み取りは全く受け付けないようで、全ビットがセットされた値が読めています。
余談
PCI configuration spaceのアクセスに使われるI/Oポートに0xcf8というのがあります。これは32ビットでアクセスされます。そして、システムリセットに使われるI/Oポートに0xcf9というのがあります。こちらは8ビットでアクセスされます。これらがどうやって区別されているかといえば、BE0#からBE3#がすべてアクティブならPCI, BE1#だけがアクティブならシステムリセットに使われるほう、ということになりそうです。当然データバス8ビットのCPUでは区別が付きませんが、PCIは32ビット以上のCPUでしか使われないので問題ないのでしょう。
BitVisorでは0xcf7などのI/Oポートに対するアクセスは見ていませんが、少し試したところでは0xcf8は32ビットでアクセスしないとアドレスが変わらなかったような... 機種依存かも知れませんが... さらに、データポートの0xcfcについても、例えば0xcfaの32ビットアクセスをしたら読めてしまうのでは、と思って試したのですが、読めなかったです。もしかしたら32ビットCPUならそれも読めてしまうのかも知れません。
なお、上の例で0x6fに対するアクセスでも、#VMEXITはしています。BitVisor内部で、0x6fに対するアクセスをパススルーで処理してしまっているだけです。例えば0xcf8の32ビットアクセスは通して0xcf9の8ビットアクセスは通さない、という風にしたくても、CPUとしてはそんな設定はできないようです。