Top

第2章 既存のコードを改造してMBRテスターを作る

前章まででKVMを使うVMはどのように作られているかを実際に簡単なVMを作ることで確認しました。

この章では、既存の扱いやすいコードを元にMBR自動テストを実現します。

なお、この章からMBRをテストするプログラムを「MBRテスター」と呼ぶことにします。

2.1 実現すること

この章で作るMBRテスターとしては、最低限以下が実現できていれば良いこととします。

2.2 手軽そうなサンプルを探す

それでは、前述した内容を実現していきます。

前章の最後に説明した通り、全て自作するのではなく、本書では手軽に使えそうなサンプルを探してそれを改造することで目的を実現してみます。

/dev/kvmを直接叩くようなプログラムとしてはQEMUが一番有名ですが、QEMUは全コード量として160万行ほどあり(2018年9月現在)、コンパイルするだけでも大変です。

そこで、「こういうことをやっているコード量が少なくてコンパイル&実行可能なサンプル無いかな」と探すとき、GitHubのコード検索はとても便利です。

https://github.com/searchにアクセスして、検索窓に含んでいてほしいコード片を書いて検索すると、GitHub内の全リポジトリから該当するソースコードを一覧表示してくれます。

「advanced search」を使うと「Advanced options」の「Written in this language」から言語の指定や、「Code options」の「Of this file size」からファイルサイズ(おそらくコード行数?)の指定もできます。

2.3 kvmulateについて

「/dev/kvmを直接使っていて」、「seabiosのバイナリも直接使っていて」、「MBRをロードして実行する」様なサンプルとしては、「kvmulate」というリポジトリのコードが良さそうです。

全コード量も5071行とQEMUに比べ圧倒的に少なく、main関数から処理を追っていって全体を把握するのもそんなに大変ではありません。

これからkvmulateを改造していく上で、この項ではkvmulateのコンパイル&動作確認と、kvmulateのソースコードを簡単に概観してみます。

2.3.1 コンパイル

まず、kvmulateのソースコードをgit clone、あるいはGitHubのページからzipでダウンロードするなどしてローカルへ配置してください。

kvmulateは、Simple DirectMedia Layer(SDL)というGUI表示するために使っているライブラリを除いて、標準的なCのライブラリとmakeしか使わないです。そのため、本書で想定しているaptが使えるLinux環境であれば、以下の2つのパッケージがインストールされていれば問題なくコンパイルできると思います。

Makefileが用意されているので、kvmulateのディレクトリ内で"make"を実行すればコンパイルできます。

2.3.2 MBRを作成

テスト対象であるMBRを用意します。

kvmulateは、同じディレクトリ内に"floppy.img"や"hdd.img"というファイル名でイメージファイルを置いておけば、それらをフロッピーディスクやハードディスクの生のイメージとして使います。

そこで、512バイトのバイナリイメージをMBRとして作成し、それらを"floppy.img"や"hdd.img"というファイル名で置いておくことにします。

MBRとしては、自身でBIOSの機能を呼び出して画面に文字を出力するようなプログラムを作成します。

サンプルは1章で紹介したリポジトリの「mbr_sample」ディレクトリに格納しています。URLは以下の通りです。

プログラムは短めのアセンブラです(リスト2.1)。

リスト2.1: mbr_sample/output_A.s

        .code16

        movb    $0x41, %al
        movb    $0x0e, %ah
        int     $0x10

        jmp     .

".code16"はアセンブラへ16ビット向けのソースコードであることを示す指定です。BIOSはCPUが16ビットのモード(リアルモード)で動作するため、アセンブラも16ビット向けで記述します。

次の3行からなるコードブロックで、BIOSの機能を呼び出して'A'という文字を画面へ描画しています。BIOSは「Basic Input/Output System」という名前の通り、基本的なI/Oのシステムを持っています。

BIOSの機能を呼び出す方法はソフトウェア割り込みで、機能ごとに割り込み番号が決められています。そしてパラメータの受け渡しには汎用レジスタを用い、機能ごとにどの汎用レジスタで何の受け渡しを行うかが決められています。

今回の場合、割り込み番号0x10が画面制御機能群で、AHレジスタにその中でどんな機能を呼び出すかを指定します。0x41がASCIIで文字出力を行う機能です。ALレジスタへは出力したい文字コード(今回の場合'A'(0x41))をASCIIで指定します。

最後の"jmp ."は無限ループで、このプログラムの戻り先は無いのでここで止めています。"."は「今この場所」を示すもので、jmpの先として指定すると「今この場所へジャンプする」事を繰り返すため無限ループになります。

他の文字を出力させたり、BIOSの別の機能を呼び出したい場合は、リスト2.1を書きかえてみてください。

Makefileとリンカスクリプト(mbr.ld)も用意しているので、mbr_sampleディレクトリで"make"を実行するだけでMBRのバイナリ(output_A.mbr)を生成できます。

2.3.3 実行するには

kvmulateは、実行する際、以下の5つが同じディレクトリに配置されている必要があります。

「seabios.rom」と「vgabios.rom」は、kvmulateのリポジトリに含まれています*1

[*1] もちろん、APTでインストールしたSeaBIOSに含まれるバイナリを使っても構いません。その際は、たいてい/usr/share/seabios/にインストールされていますので、この中にある「bios.bin」を「seabios.rom」へ、「vgabios-stdvga.bin」を「vgabios.rom」へリネームして使ってください。

また、「floppy.img」と「hdd.img」は共に同じもので構いません。本書では、MBRのバイナリ(例えば、前節で作ったoutput_A.mbr)を「floppy.img」と「hdd.img」という2つのファイルへコピーして配置しています*2

[*2] kvmulate内の処理として、floppy.imgを使用する場合でもhdd.imgのファイルチェックをするコードパスに入るためです。hdd.imgを都度用意するのが面倒であれば、"hdd.img"をopenしているところ辺りの処理を削除すれば良いです。

2.3.4 kvmulateのコード構成

kvmulateは図2.1の構成になっています*3

[*3] 本書の改造の範囲外(DMAなど)は省略しています。

kvmulateの構成

図2.1: kvmulateの構成

system.cに定義されているsys_create関数(とそこから呼び出される関数)でKVM_RUN実行前の準備(VM作成)を行い、同じくsystem.cに定義されているsys_run関数(とそこから呼び出される関数)でKVM_RUNとKVM_RUNから戻ってきた時のハンドリングを行います。

sys_create関数からは、sys_tという構造体の型のpSysへVCPUとVMの設定を行い、io_handler_tという同じく構造体型のiohandler配列へIOポート毎のハンドラ設定を行います。

また、sys_run関数からはKVM_RUNを行い、IOでEXITしてきた際にはiohandler配列の該当するIOポートの要素を参照して登録されているハンドラを呼び出します。

2.4 改造する

それでは、kvmulateを改造してMBRテスターを実現してみます。

以降では変更箇所を説明しますが、patchファイル(diff)がほしい場合は以下の筆者のページにGistへのリンクを掲載しますので、参照してみてください。

主に変更するファイルはMakefile(リスト2.2)とGUI表示を行うviewer.c(リスト2.3)です。あと、kvmulateの標準出力へのログ出力等を抑制します。

リスト2.2: kvmulate/Makefile

DEPS := $(subst .o,.d,$(OBJS))
SRCS := $(subst .o,.c,$(OBJS))

all: mbr_tester  # 変更

mbr_tester: $(OBJS)  # 変更
       $(CC) $(CFLAGS) -pthread -o $@ $^  # -lSDLを外す

%.o: %.c
       @echo "CC       $<"

clean:
        @echo "CLEAN"
        @$(RM) -r $(OBJS) $(DEPS) mbr_tester boot.bin  # 変更

SDLを外す他に、生成する実行ファイル名も"mbr_tester"へ変更しました。

viewer.cについてはSDLの処理をがっつりと消してしまい、リスト2.3のようにしました。

リスト2.3: kvmulate/devices/x86/vga/viewer.c

#include <unistd.h>
#include <pthread.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "devices/x86/vga/vga.h"
#include "devices/x86/vga/font.h"
#include "log.h"

static uint8 test_ch(uint8 ch)
{
  return ch == 'A';
}

#define TEST_FAILURE_TH 200
static void updateTextmode(vgactx_t *pVGA) {
  int x,y;
  uint8 ch,attr;
  uint32 vram_off;

  vram_off = (pVGA->crtc_reg[0xC] << 8) | pVGA->crtc_reg[0xD];
  vram_off *= 2;

  static uint32 test_counter = 1;
  for(y=0;y<25;y++) {
    for(x=0;x<80;x++) {
      ch   = pVGA->pVRAM[0][0x18000 + vram_off + (y*80+x)*2+0];
      attr = pVGA->pVRAM[0][0x18000 + vram_off + (y*80+x)*2+1];

      if (y > 4) {
        if (test_ch(ch)) {
          printf("pass(test_counter=%d)\n", test_counter);
          exit(EXIT_SUCCESS);
        } else if (test_counter > TEST_FAILURE_TH) {
          printf("fail(test_counter=%d)\n", test_counter);
          exit(EXIT_FAILURE);
        }
      }
    }
  }
  test_counter++;
}

static void *viewer_thread(void *pArg) {
  vgactx_t *pVGA = (vgactx_t*)pArg;
  int bDone = 0;

  LOG("pVRAM[0] = %p",pVGA->pVRAM[0]);

  while(!bDone) {
    updateTextmode(pVGA);

    usleep(1000000LL / 60LL);
  }

  return NULL;
}

int x86_vga_viewer_init(vgactx_t *pVGA) {
  pthread_t thid;

  LOG("pVRAM[0] = %p",pVGA->pVRAM[0]);
  pthread_create(&thid,NULL,viewer_thread,pVGA);

  return 0;
}

viewer.cの処理が呼び出される流れは以下の通りです。

  1. sys_create関数が、IOハンドラの初期化などのためにdevices_register_all関数(devices/devices.c)を呼び出す
  2. devices_register_all関数からviewer.cのx86_vga_viewer_init関数が呼ばれる
  3. x86_vga_viewer_init関数はスレッドを生成し、viewer_thread関数を実行させる
  4. viewer_thread関数は60fpsで画面更新(updateTextmode関数)を行う
  5. updateTextmode関数では、文字出力等によりVRAMの領域(pVGA->pVRAM)に格納された文字を描画する(ただし、描画処理は丸ごと削っている)

そのため、updateTextmode関数で、描画しようとしている文字を拾えば、出力される文字のテストができます。

ここでは、test_chという関数を追加して、MBRが"A"という文字を出力することをテストしています。5行目以降(y > 4)の文字をテスト対象にしているのは、4行目まではBIOSの出力があるからです。

また、変数test_counterは、updateTextmode関数呼び出しの回数、すなわち60fpsでの画面描画回数です。定数TEST_FAILURE_TH(200)の回数を超えてもtech_ch関数をpassしないようであれば、テストはfailと判定します。

最後に、標準出力へのログ出力等を抑制します(リスト2.4、リスト2.5)。

リスト2.4: kvmulate/log.c

#include <stdio.h>
#include <string.h>
#include <stdarg.h>

#include "log.h"

//int LogLevel = LOGLEVEL_INFO;
//int LogLevel = LOGLEVEL_DEBUG;        /* コメントアウト */
int LogLevel = -1;      /* 追加 */

リスト2.5: kvmulate/devices/x86/bios_debug.c

static void  biosdebug_outb(struct io_handler *hdl,uint16 port,uint8 val) {
  /* putchar(val); */    /* コメントアウト */
}

リスト2.4はkvmulateのログ出力で、LogLevelが-1だと何も出力されなくなります。

リスト2.5について、biosdebug_outb関数は、SeaBIOSがデバック情報を出力する際のIO命令(IOアドレス0x0402)のハンドラです。これも関数内のputchar関数をコメントアウトすることで抑制しています。

なお、MBRから"int 0x10"でBIOSの機能を呼び出して出力する文字についてはBIOSのデバッグ出力では出力されません。そのため、先述した「5行目以降にMBRが出力させる文字が表示される」という確認は、viewer.cのupdateTextmode関数で標準出力に行番号付きで文字を出力させると分かりやすいです。本章最後の補足にまとめているので興味があればご覧ください。

2.5 動作確認

それでは、動作させてみます。コンパイルし、生成された"mbr_tester"バイナリを実行すると、以下のようにテストが通り、終了ステータス0で終了できていることを確認できます。

$ ./mbr_tester && echo 'kvmulate is succeeded.' || echo 'kvmulate is failed.'
pass(test_counter=141)
kvmulate is succeeded.

また、試しにsample_Aの出力文字、あるいはmbr_tester側のテスト内容のいずれかのみを変更すると、以下のようにテストが失敗し、終了ステータスが0以外で終了できていることを確認できます。

$ ./mbr_tester && echo 'kvmulate is succeeded.' || echo 'kvmulate is failed.'
fail(test_counter=201)
kvmulate is failed.

2.6 補足: GUI上の内容を標準出力へ出す

デバッグ用途も兼ねて、GUI画面上に表示されていたメッセージ等を行番号付きで標準出力へ出力させるには、viewer.cのupdateTextmode関数をリスト2.6のように改造すれば良いです。

リスト2.6: devices/x86/vga/viewer.c

static void updateTextmode(vgactx_t *pVGA) {
  int x,y;
  uint8 ch,attr;
  uint32 vram_off;

  vram_off = (pVGA->crtc_reg[0xC] << 8) | pVGA->crtc_reg[0xD];
  vram_off *= 2;

  static uint32 test_counter = 1;
  printf("\x1b[2J");  /* 追加 */
  printf("----------\n");     /* 追加 */
  for(y=0;y<25;y++) {
    printf("%02d: ", y);      /* 追加 */
    for(x=0;x<80;x++) {
      ch   = pVGA->pVRAM[0][0x18000 + vram_off + (y*80+x)*2+0];
      attr = pVGA->pVRAM[0][0x18000 + vram_off + (y*80+x)*2+1];

      /* test_ch関数呼び出し周りの処理は削除 */

      putchar(ch);      /* 追加 */
    }
    putchar('\n');      /* 追加 */
  }
  printf("----------\n");     /* 追加 */
}

これで「'A'だけを出力する」MBRを実行すると図2.2のようになります。

GUIの内容をシェル上へ出力するよう改造

図2.2: GUIの内容をシェル上へ出力するよう改造

このように、y=5行目以降にMBRで出力させる文字が表示されるため、前述のtest_ch関数はy>4の条件で呼び出していました。


Top