x64 UEFI 対応 OS 自作入門 ~実機で動作する OS を作ってみよう~
  • まえがき
  • 第1章:準備
  • 第2章:32 ビット整数式の x64 JIT コンパイラを試作する
    • 2.1 コンパイラを自作する理由
    • 2.2 全体の処理の流れ
    • 2.3 コーディングの方針
    • 2.4 字句解析
    • 2.5 構文解析
    • 2.6 意味解析
    • 2.7 中間コード(WebAssembly)生成
    • 2.8 x64 コード生成
    • 2.9 デバッグとテストコード
    • 2.10 改善を検討すべき点
    • 2.11 参考文献・資料
  • 第3章:クロス開発環境を作る(1)
  • 第4章:UEFI アプリを自作開発環境でビルドする
    • 4.1 UEFI
      • 4.1.1 初めての UEFI アプリ
      • 4.1.2 GPT と MBR
      • 4.1.3 UEFI セットアップ画面
      • 4.1.4 FreeType の利用
      • 4.1.5 UEFI セキュアブート
    • 4.2 まとめ
    • 4.3 参考文献・資料
  • 第5章:クロス開発環境を作る(2)
  • 第6章:シングルタスク OS をデバッグ可能にする
  • 第7章:対応するデバイスを増やす
  • 第8章:マルチタスク/マルチコア対応
  • 第9章:インストーラを作る
  • 第10章:開発環境を移植する
  • 第11章:TCP/IP・印刷機能を作る
  • 第12章:GitHub サポート機能を作る
  • 第13章:日本語入力機能を作る
  • 第14章:ハイパーバイザを作る
  • あとがき
  • 索引
GitBook提供
このページ内
  • 2.9.1 バグ修正の例(1)
  • 2.9.2 バグ修正の例(2)
  • 2.9.3 自動テスト
  1. 第2章:32 ビット整数式の x64 JIT コンパイラを試作する

2.9 デバッグとテストコード

前へ2.8 x64 コード生成次へ2.10 改善を検討すべき点

最終更新 6 年前

書籍や blog 記事などでは、コンパイラのデバッグやテストについて、あまり解説されているものを見かけないため、簡単な例を説明します。

2.9.1 バグ修正の例(1)

先に修正したソースコードのパッチの場所を書いておきます。

以下のソースコードをコンパイルして出力された WebAssebly バイナリ表現と、以下のソースコードを WebAssembly テキスト表現に 書き直したものを wabt の wat2wasm コマンドで WebAssebly バイナリ表現に変換したものを比較した際に、関数のローカル変数の数が一致しないという問題が見つかりました。

int32_t value1 = (1 + 2) * 3;
int32_t value2 = 2 + (3 * value1);
value1 = value2 + 100;

上記のソースコードに対応する文法は、以下の通りです。

Program -> Statement+
Statement -> Type? variable '=' Expression ';'
Expression -> Term (('+' | '-') Term)*
Term -> Factor (('*' | '/') Factor)*
Factor -> '(' Expression ')' | ('+' | '-')? (variable | constant)
Type -> int32_t

意味解析で、変数を定義する文法:Statement -> Type variable '=' Expression ';' が 3 回、認識されたことが中間コード生成に伝わり、 関数のローカル変数の数 が 3 であると出力されたものと、推定しました。wabt の wat2wasm コマンドでは 2 が出力されていました。

ソースコードを構文解析して得られた構文木は、デバッグ用途のために int_calc_parse_tree.log ファイルに出力できるようになっています。 以下は、int_calc_parse_tree.log ファイルの内容を抜粋したものです。変数を定義する文法が 3 回出現していて、3 回目は先頭に int32_t ではなく value1 が記録されているため 不正な文法であることが判明しました。代入文の文法:Statement -> variable '=' Expression ';' を、変数を定義する文法:Statement -> Type variable '=' Expression ';' として 内容を誤って構文木を出力するプログラム・ミスを修正することができました。

=== Dump all parse tree. ===

   {
    TP_PARSE_TREE_GRAMMER_PROGRAM
    TP_PARSE_TREE_TYPE_NODE
       {
        TP_PARSE_TREE_GRAMMER_PROGRAM
        TP_PARSE_TREE_TYPE_NODE
           {
            TP_PARSE_TREE_GRAMMER_STATEMENT_2
            TP_PARSE_TREE_TYPE_TOKEN
               {
                TP_SYMBOL_ID
                TP_SYMBOL_TYPE_INT32
                 member_line(0)
                 member_column(0)
                 member_string(int32_t)
                 member_i32_value(0)
               }

        TP_PARSE_TREE_TYPE_NODE
           {
            TP_PARSE_TREE_GRAMMER_STATEMENT_2
            TP_PARSE_TREE_TYPE_TOKEN
               {
                TP_SYMBOL_ID
                TP_SYMBOL_TYPE_INT32
                 member_line(1)
                 member_column(1)
                 member_string(int32_t)
                 member_i32_value(0)
               }

    TP_PARSE_TREE_TYPE_NODE
       {
        TP_PARSE_TREE_GRAMMER_STATEMENT_2
        TP_PARSE_TREE_TYPE_TOKEN
           {
            TP_SYMBOL_ID
            TP_SYMBOL_ID_INT32
             member_line(2)
             member_column(1)
             member_string(value1)
             member_i32_value(0)
           }

2.9.2 バグ修正の例(2)

先に修正したソースコードのパッチの場所を書いておきます。

前項のバグは、変数 value1 が多重定義されていることを検出できていれば、コンパイル時にエラーになるため、その時点で気づくことができるはずでした。 参照しやすいように、以下にソースコードを再掲します。

int32_t value1 = (1 + 2) * 3;
int32_t value2 = 2 + (3 * value1);
value1 = value2 + 100;

意味解析で、変数名をハッシュ表に登録する際に、すでに変数名が登録されている場合にエラーにしていなかったプログラム・ミスを修正することができました。 前節のバグ修正で、代入文:value1 = value2 + 100; が変数定義文にコンパイルされる不具合は解消していますから、先頭に int32_t 型を付けてコンパイルしてみると、 以下のようにコンパイル時に変数の多重定義エラーを検出することができるようになりました。

C:\>type source.txt:
int32_t value1 = (1 + 2) * 3;
int32_t value2 = 2 + (3 * value1);
int32_t value1 = value2 + 100;

C:\>int_calc_compiler.exe -l source.txt
tp_semantic_analysis.c(278): ERROR: Duplicate DEFINED_REGISTER_OBJECT at register_defined_variable function.
tp_compiler.c(457): ERROR: Compile failed.

2.9.3 自動テスト

プログラムの動作を非常に具体的かつ詳細にテストするコードを書いている時間はなかったため、計算結果を正常値と比較することと、 スナップショット(出力されるバイナリや log など)を取得可能にし、スナップショットを比較することで、ソースコードの修正が以前の正常な動作を変えていないことを確認可能にしました。 スナップショットは、スナップショット格納フォルダ(test_YYYY-MM-DD_nnn)に出力されます。

typedef struct test_case_table_{
    uint8_t* member_source_code;
    int32_t member_return_value;
}TEST_CASE_TABLE;

static TEST_CASE_TABLE test_case_table[] = {

    { "int32_t value1 = 1 + 2;\n", 3 },
    { "int32_t value1 = 1 - 2;\n", -1 },
    { "int32_t value1 = 1 * 2;\n", 2 },
    { "int32_t value1 = 4 / 2;\n", 2 },

    { "int32_t value1 = 1;\n"
    "int32_t value2 = value1 + 2;\n", 3 },

    { "int32_t value1 = 1;\n"
    "int32_t value2 = value1 - 2;\n", -1 },

    { "int32_t value1 = 1;\n"
    "int32_t value2 = value1 * 2;\n", 2 },

    { "int32_t value1 = 4;\n"
    "int32_t value2 = value1 / 2;\n", 2 },

    { "int32_t value1 = 1;\n"
    "int32_t value2 = 2 + value1;\n", 3 },

    { "int32_t value1 = 1;\n"
    "int32_t value2 = 2 - value1;\n", 1 },

    { "int32_t value1 = 1;\n"
    "int32_t value2 = 2 * value1;\n", 2 },

    { "int32_t value1 = 2;\n"
    "int32_t value2 = 4 / value1;\n", 2 },

    { "int32_t value1 = 1;\n"
    "int32_t value2 = 2;\n"
    "int32_t value3 = value1 + value2;\n", 3 },

    { "int32_t value1 = 1;\n"
    "int32_t value2 = 2;\n"
    "int32_t value3 = value1 - value2;\n", -1 },

    { "int32_t value1 = 1;\n"
    "int32_t value2 = 2;\n"
    "int32_t value3 = value1 * value2;\n", 2 },

    { "int32_t value1 = 4;\n"
    "int32_t value2 = 2;\n"
    "int32_t value3 = value1 / value2;\n", 2 },

    { "int32_t value1 = (1 + 2) * 3;\n"
    "int32_t value2 = 2 + (3 * value1);\n"
    "value1 = value2 + 100;\n", 129 },

    { NULL, 0 }
};

Windows の PowerShell やコマンド・プロンプト、Python などの既存のスクリプト言語に依存してしまうと、自作 OS 上で動作させることが難しくなるため、 テスト・ケースと、自動で複数のテストを一括して実行するロジックをコンパイラ本体に内蔵 しています。テスト・ケースは、以下の通りです。

int_calc_compiler/src/lib/tp_compiler/tp_semantic_analysis.c
int_calc_compiler/src/lib/tp_compiler/tp_compiler.c
int_calc_compiler/src/lib/tp_compiler/tp_make_parse_tree.c
内容不一致