BitVisor を使ってHPET に悪戯してみる (BitVisor でMMIOをフックする一例)


はじめに

この記事では,BitVisor を使って,HPET に少し細工をする例を紹介します.

注意事項

今回書いた記事に載せたソースコードは,BitVisor 1.3 の頃に書いたコードです.
しかも,動作確認もせず,必要そうなところだけ切り出してるので,そのまま動く保証がないです...
(試そうとしたら,使えるマシンにHPET らしきものが無くて,動作確認を断念しました,ごめんなさい.)

そもそもHPET って何?

HPET はHigh Precision Event Timer の略で,タイマデバイスの一つです.

余談

HPET ,今はあまりデフォルトで使われてないような気がします.
Linux はどういう仕組みか知りませんが,最も精度がよいタイマを探し,それを使うようにしています.
Nehalem 世代のCPU だと,TSC がTurbo boost の影響で揺れていたので,HPET の方が精度が高く,clocksource のデフォルトとして使われていたのを見ました.
しかし,sandy bridge 世代以降のCPU だと,TSC が安定し,clocksource としてはTSC がデフォルトで使われることが多いようです.

どんな悪戯をするのか?

HPET はタイマ(正確にはコンパレータ)を複数持てるような仕様になっています.
dmesg | grep -i hpet なんてやると,HPET 関連のdmesg の中で,そのタイマの数を見つけることができるかと思います.
このタイマの数を一つ減らして見せようと思います.

$ dmesg | grep -i hpet
[    0.000000] ACPI: HPET 00000000cffba760 000038 (v01 DELL   OEMHPET  20100910 MSFT 00000097)
[    0.000000] ACPI: HPET id: 0x8300 base: 0xfed00000
[    0.000000] hpet clockevent registered
[    0.016000] tsc: using HPET reference calibration
[    0.284014] hpet0: at MMIO 0xfed00000, IRQs 2, 8, 0, 0
[    0.284016] hpet0: 4 comparators, 32-bit 14.318180 MHz counter
[    0.287141] Switched to clocksource hpet
[    1.465067] rtc_cmos 00:02: alarms up to one month, y3k, 114 bytes nvram, hpet irqs

[ 0.284016] hpet0: 4 comparators, 32-bit 14.318180 MHz counter から,4つのタイマがあることがわかります.
これを,BitVisor を使って3個に見せるという地味な悪戯です.

なぜそんな記事を書くのか?

(昔,開発の練習でやったことをそのままネタとして書いて1日分ごまかそうとかそういうわけでは,いえですね,はい.)
まぁ,BitVisor でMMIO をフックして,その中身を入れ替える例として単純でちょうど良いかなと思ったからです.
内容は地味ですが,容易に確認できますし,MMIO のフック以外に複雑な操作が必要ないので.

ソースコード

追加するソースコードは以下の通りです.
(要らないprintf やらマクロやらがたくさん残ったままです...)

core/hpet.h
#define HPET_BASE_ADDR 0xfed00000
#define HPET_COUNTER 0x0f0
#define HPET_Tn_CMP(n) (0x108+0x20*n)
#define HPET_HZ (14.318180*1e6)
#define s2ms(s) (s*1e3)
#define s2us(s) (s*1e6)
#define s2ns(s) (s*1e9)
void reg_hpet_mm_handler(void);
core/hpet.c
#include "mmio.h"
#include "../crypto/chelp.h"
#include "printf.h"
#include "mm.h"
#include "current.h"
#include "gmm_access.h"
#include "initfunc.h"
#include "spinlock.h"
#include "hpet_hook.h"

spinlock_t hpet_lock;

union hpet_gcidr{
  struct {
    unsigned int rev_id : 8;
    unsigned int num_tim_cap : 5;
    unsigned int count_size_cap : 1;
    unsigned int reserved : 1;
    unsigned int leg_route_cap : 1;
    unsigned int vender_id : 16;
  }s;
  u32 v;
}typedef hpet_gcidr;

static int 
hpet_mm_handler(void *data, phys_t gphys, bool wr, void *buf, uint len, u32 flags){

    unsigned int ret_data;
    phys_t hphys;
    hpet_gcidr gcidr;
    printf("guest's physical address is 0x%016x\n", (unsigned int)gphys);
    hphys = current->gmm.gp2hp (gphys, NULL);
    printf(" VMM's physical address is 0x%016x\n", (unsigned int)hphys);
    printf("I/O type: %s\n", (wr) ? "WRITE" : "READ");
    printf("Length is: %d\n", len);
    printf("Flags is: 0x%016x\n", flags);

    if(buf == NULL){
        printf("buf is NULL pointer\n");
        return 0;
    }
    if(!wr){
        if (len == 1)
        {
            read_hphys_b(hphys, &ret_data, flags);
            *((unsigned char*)buf) = (unsigned char)ret_data;
        }
        else if (len == 2)
        {
            read_hphys_w(hphys, &ret_data, flags);
            *((unsigned short *)buf) = (unsigned short)ret_data;
        }
        else if (len == 4)
        {
            read_hphys_l(hphys, &ret_data, flags);
            printf("[by fukai] Read data is 0x%08x\n", ret_data);
            gcidr.v = (u32)ret_data;
            printf("[by fukai] number of Timer is 0x%d\n", gcidr.s.num_tim_cap);
            gcidr.s.num_tim_cap --;
            *((unsigned int *)buf) = (unsigned int)gcidr.v;
            printf("[by fukai] return data is 0x%08x\n",(unsigned int)gcidr.v);
        }

        else if (len == 8)
        {
            read_hphys_q(hphys, &ret_data, flags);
            *((unsigned long int *)buf) = (unsigned long int)ret_data;
        }

        return 1;
    }else{
        printf("[by fukai] debug point 999\n"); 
        return 0;
    }
}


void 
reg_hpet_mm_handler(void){
    mmio_register(0xFED00000, 0x4, hpet_mm_handler, NULL);
}

これで,reg_hpet_mm_handler() 関数をBitVisor の初期化の適当なところ(vt_mainloop() のwhile ループ前とか) に差し込めば動くと思います.

ソースコード解説

reg_hpet_mm_handler()

中身は1行, mmio_register(0xFED00000, 0x4, hpet_mm_handler, NULL); だけです.
この, mmio_register() とうのが,MMIO フックの鍵です.
- 第1引数は,フックしたいMMIO の開始アドレス
- 第2引数は,フックしたいMMIO 領域の長さ
- 第3引数は,フックした時に呼ばれるコールバック関数
- 第4引数は,コールバック関数に渡すデータ(だと思います)

HPETにかぎらずですが,フックするべきMMIOのアドレスは,仕様に沿って決まっていたり,そのアドレスを取得する方法があると思います.
しかし,それが面倒で,とりあえず目の前のマシンで試したいという時は,実験用マシンで以下のようなコマンドを叩いて,MMIOの一覧を出すと良いかと思います.

$ cat /proc/iomem | grep -i hpet
fed00000-fed003ff : HPET 2

これを見ると,HPETのMMIOは0xFED00000 から始まっていることがわかります.
また,どうして長さが4なのかというと,今回の処理では,最初4byte だけフックできればいいからです.
この最初4byte というのは,HPET のGeneral Capabilities and ID Register というレジスタがマップされており,ここにHPETのタイマの数が書かれているからです.
今回は,この値をOSが読みに来た時に,その値を1減らしてゲストに返すような処理をします.

hpet_mm_handler()

フックした時に呼ばれるコールバック関数です.

まずは引数について
- 第1引数は,何かしらのデータです.多分,mmio_register() の第4引数がここに来ます.
- 第2引数は,実際にフックしたMMIO のアドレス.アドレスは,guest physical アドレスです.
- 第3引数は,MMIO がwrite かread かを示すものです.true ならwrite です.
- 第4引数は,MMIO でデータを受け渡しするためのバッファです.read なら,この値をちょろまかすと,その値がゲストに見えます.また,write なら,この値をちょろまかしておくと,デバイスにはちょろまかした値が書き込まれます.
- 第5引数は,MMIO のread or write の長さです.例えば,movq なら 8byte 単位のメモリアクセスなので,8が入ります.
- 第6引数は,... なんだっけ? ど忘れしました,時間があれば追記しときます.

次にこの関数の戻り値についてです.
- 0 なら,この関数から処理が戻ったあと,BitVisor がフックした MMIO をエミュレートします.
- 非零なら,この関数から処理が戻ったあと,BitVisor は MMIO をエミュレートしません.

では,中の処理を順に見ます.
printf () の呼び出しについては,無視で.
hpet_gcidr gcidr; というのは,General Capabilities and ID Register のビットフィールドに合わせたユニオン型の変数です.

hphys = current->gmm.gp2hp (gphys, NULL); でhphys という変数に gphys に対応する host phisycal アドレスが入ります.
ちなみに,hphys はhost physical, gphys はguest physical の略です.

今回,対象にするのは,4byte 単位のread だけです.
なぜなら,General Capabilities and ID Register は4byte のレジスタで,4byte 単位のアクセスをされるからです.
この関数では,OSが 4byte で read してきた時,その値を読みだして,General Capabilities and ID Register の NUM_TIM_CAP フィールドの値を一つ減らした後,値をbuf に格納します.
また,1byte, 2byte, 8byte 単位の read は単純に値を読みだして,buf に入れているだけです.

read の際は,return 1 とし,この関数を抜けた後にMMIOのエミュレートをしないようにします.
この関数のなかで,既にフックしたMMIOに変わる処理を行ったためです.
一方,write の際は return 0 として,この関数を抜けた後にMMIOのエミュレートをするようにします.

おわりに

気がついたら,時間がなく,慌てて書いたので,完全に駄文です,ごめんなさい.
時間があるときに直します.