デバッグとテスト

このページの目次

ビルド成功、警告なし実行、テストケースのSKIPは、それぞれ異なる結果を意味します。カーネル検証では、⁠入力、実行パス、環境前提、結果を1つの記録に収める必要があります。

参照目的参照箇所
実行パスの特定ログとDynamic Debug
境界条件の検証KUnit、kselftest、sanitizer、ファジーテスト
失敗分岐と納品証拠障害注入、再現記録
再現可能な問題から対象的な検証へ 環境と再現入力を保存 カーネルコミット、設定、モジュール、ハードウェアまたはVMパラメータ 1 失敗メカニズムの特定 ログ、トレース、sanitizer、ロック依存関係、ブレークポイント 2 最小化と修正 トリガー条件とエラーパスを保持 3 回帰と境界検証 正例、反例、並行処理、障害注入 4 カバレッジと1回限りのパスは正しさの証明ではありません。ツールは検証対象のメカニズムに合わせて選択すべきです。

printk とコンソールレベル

pr_err、pr_info、pr_debug などはログのエントリポイントを提供し、dev_* 形式はデバイスコンテキストを伴います。ログがカーネルバッファに記録される段階と、最終的にコンソールに出力される段階は異なります。console_loglevel はコンソール出力をフィルタリングするだけで、低いレベルの記録をすべて削除するわけではありません。

ログレベルの数値が小さいほど重大です。console_loglevel=4 は通常、数値が4より小さいメッセージ、つまり error およびそれより重大なレベルを出力します。この設定を「システムにはこれらのログしか存在しない」と記述すべきではありません。高頻度パスのログはタイミングとパフォーマンスを変更するため、レート制限を行うか、適切なトレースポイントに切り替える必要があります。

Dynamic Debug で既存の呼び出しポイントを制御

dynamic debug は、対応する呼び出しポイントがサポートされ、コンパイルされている場合に限り、モジュール、関数、ファイル、行番号、またはその他の条件に基づいて pr_debug/dev_dbg などの情報を有効にします。任意のデバッグ文がない関数に、ありもしないエントリログを追加することはできません。

現在のドキュメントでは /proc/dynamic_debug/control を提供しています。debugfs が有効化され、マウントされている場合は /sys/kernel/debug/dynamic_debug/control も存在する可能性があります。実際存在する制御ファイルを読み取り、エントリと元のフラグを確認してから、限定的な範囲で変更を行ってください。Dynamic debug

例えば、コマンド文字列 module my_driver +p は、対応するモジュールエントリを有効にすることを意味します。-p は印刷を無効にし、f/l/t などのフラグは関数、行番号、スレッド情報を追加します。復元時は以前の状態に戻し、他者が有効にしたデバッグポイントを無条件に消去しないでください。

KUnit と kselftest は異なる境界をチェックする

KUnit はカーネル環境内で内部関数またはサブシステムのロジックを検証し、UML、QEMU、またはテスト用カーネルで実行できます。kselftest は通常、ユーザー空間からカーネルインターフェースと動作をチェックし、一部のスイートはテストモジュールと連携します。これらは補完関係にあります。KUnit 入門、kselftest

KUnit には KUNIT_CASE 配列だけでなく、スイートの登録と Kconfig/Kbuild での接続も必要です。以下に、バッファの範囲チェックを例として挙げます。容量が capacity で、アクセス範囲が [offset, offset+length) の場合、長さが0で offset がちょうど capacity に等しいことは許可されますが、範囲外アクセスは禁止されます。offset + length <= capacity を直接判断すると、符号なし加算のラップにより、不正な範囲が合法と誤認される可能性があります。

kb_range_test.c として保存:

#include <kunit/test.h>
#include <linux/module.h>
#include <linux/limits.h>

static bool range_fits(size_t capacity, size_t offset, size_t length)
{
    return offset <= capacity && length <= capacity - offset;
}

static void range_boundary_test(struct kunit *test)
{
    KUNIT_EXPECT_TRUE(test, range_fits(4096, 4092, 4));
    KUNIT_EXPECT_FALSE(test, range_fits(4096, 4092, 5));
    KUNIT_EXPECT_TRUE(test, range_fits(4096, 4096, 0));
    KUNIT_EXPECT_FALSE(test, range_fits(4096, 4097, 0));
    KUNIT_EXPECT_TRUE(test, range_fits(0, 0, 0));
}
static void range_overflow_test(struct kunit *test)
{
    KUNIT_EXPECT_FALSE(test, range_fits(4096, SIZE_MAX - 3, 4));
    KUNIT_EXPECT_TRUE(test, range_fits(SIZE_MAX, SIZE_MAX, 0));
    KUNIT_EXPECT_FALSE(test, range_fits(SIZE_MAX, SIZE_MAX, 1));
}
static struct kunit_case range_cases[] = {
    KUNIT_CASE(range_boundary_test),
    KUNIT_CASE(range_overflow_test),
    {}
};
static struct kunit_suite range_suite = {
    .name = "kb-range",
    .test_cases = range_cases,
};
kunit_test_suite(range_suite);
MODULE_LICENSE("GPL");

第一グループは排他的上限と空の範囲の契約をチェックし、第二グループは最大整数付近での加算リスクをチェックします。offset <= capacity は、後の減算がアンダーフローしないことも保証します。C言語の && のショートサーキット評価により、2番目の項はこの前提が成立した場合のみ計算されます。このヘルパー関数は数値範囲のみを検証し、ポインタの有効性、ページへのアクセス可能性、またはデバイスがDMAを実行する権限を持っているかどうかは保証しません。

テストソースディレクトリに接続する際、対応する Kconfig エントリと Kbuild ルールを追加し、上位の設定とビルドでこのディレクトリを含めてください:

config KB_RANGE_KUNIT_TEST
    tristate "Range boundary tests" if !KUNIT_ALL_TESTS
    depends on KUNIT
    default KUNIT_ALL_TESTS
obj-$(CONFIG_KB_RANGE_KUNIT_TEST) += kb_range_test.o

このテストが接続されたクリーンなソースツリーでは、独立したビルドディレクトリで .kunitconfig を使用して CONFIG_KUNIT=y と CONFIG_KB_RANGE_KUNIT_TEST=y を有効にし、スイート名でフィルタリングできます。フィルタはビルドに含まれているテストのみを選択し、設定の有効化に代わるものではありません。

./tools/testing/kunit/kunit.py run --build_dir=.kunit-range --kunitconfig=.kunitconfig kb-range

デフォルトの UML パスは、現在のホストにテストモジュールをロードするのではなく、ユーザー空間Linuxテストカーネルをビルドして実行します。ソースツリーに互換性のないビルド成果物が既に存在する場合は、別途準備したクリーンなソースコピーを使用してください。チュートリアルコマンドのために、メンテナンス中のカーネルツリーで直接 mrproper を実行しないでください。KUnit 設定、ビルド、フィルタリング

組み込みテストとモジュールテストの実行タイミングは異なります。kunit.py はテストの設定、ビルド、実行を支援できますが、初回実行には完全なテストカーネルのビルドが必要になる場合があります。単にCファイルをコンパイルしただけでは、テストが実行されたことにはなりません。

結果の解釈:SKIP と PASS を別々に記録

selftests の TARGETS はスイートをフィルタリングできますが、ネットワーク、BPF などのテストには対応するカーネル設定、権限、依存関係が必要であり、テスト環境を変更する可能性があります。変更に関連するスイートを優先して実行し、SKIP の理由を記録してください。「すべてスキップ」は「すべてパス」ではありません。

sanitizer、ロックチェック、ブレークポイントはそれぞれ証拠を提供する

KASAN は適用可能な範囲外アクセスと解放後使用(use-after-free)に焦点を当て、KCSAN はデータ競合の発見を支援し、lockdep はロック依存関係などの制約をチェックします。これらは設定、実行パス、サンプリングメカニズムの影響を受けるため、問題が報告されなかったことをエラーがないことの証明として扱うことはできません。

KGDB は、適切な設定とデバッグリンクの下で GDB を使用してカーネルをデバッグすることを許可します。SysRq-g はターゲットカーネルを停止させてデバッガを待機させる可能性があり、回復手段があるテスト環境に適しています。日常の読み取り専用診断コマンドとして直接実行すべきではありません。

ブレークポイントとログはスケジューリングのタイミングを変更します。再現が難しい競合状態は、トレースポイント、最小入力、繰り返しトリガーと組み合わせて、発生順序を保存し、最後のスタックのみを保持しないでください。

syzkaller と LTP

syzkaller はシステムコール記述、リソース関係、カバレッジフィードバックを使用してプログラムを生成および変異させ、設定されたターゲット環境でクラッシュ、ハングなどの異常を検出し、最小化と再現を試みます。QEMU は一般的な実行バックエンドですが、唯一ではありません。自動バイセクトにも対応するインフラストラクチャが必要であり、任意の実行をすべてバイセクトを含むとは限りません。syzkaller

LTP はシステムインターフェースなどの回帰テストコレクションを提供し、カーネルとlibcの相互作用の検証を補完するのに適しています。テストケースの失敗は、カーネル、環境、権限、またはテスト前提に起因する可能性があるため、総失敗数だけを報告するのではなく、具体的な結果とバージョンを保持してください。LTP

障害注入:制御不能な失敗を検証可能な入力に変える

メモリ割り当て、I/O、関数戻り値などの注入ポイントは、クリーンアップ、リトライ、ロールバックパスをチェックするのに役立ちます。フレームワークの probability、interval、times、task-filter およびフィルタ条件は、トリガーに共同で影響します。failslab/interval を単に 100 に設定しても、任意の割り当ての100回ごとに必ず1回失敗するとは限りません。障害注入

段階確認すべき事実
注入前ターゲットタスク/パスが限定され、ベースライン動作が明確
注入中予期された失敗が実際に発生し、クリーンアップとロールバックが検証可能
注入後リソースが解放され、関連パラメータが復元

理論的な例では、現在のホストでグローバル注入を有効にする必要はありません。テストVMの方が再現と回復が容易です。

取得順序に従って1つずつ失敗し、所有しているリソースのみを解放するか確認

初期化が context、buffer、registration を順に取得し、すべて成功してからオブジェクトを呼び出し元に公開すると仮定します。最初の割り当てのみを1回失敗させるのではなく、各ステップでの失敗を個別にテストする必要があります。

失敗位置失敗前に所有ロールバック順序
contextなし取得していないリソースを解放しない
buffercontextcontext
registrationcontext、bufferbuffer → context
すべて成功して終了3つのリソース、かつオブジェクトが公開済み外部公開を停止してから、リソース依存関係に従ってクリーンアップ
3番目の取得失敗:前2ステップで既に取得したリソースのみを回収 context を取得 成功 buffer を取得 成功 registration を取得 失敗、所有権を取得せず buffer を解放 context を解放 registration の取得に失敗したため、解放しない。オブジェクトはまだ公開されていないため、呼び出し元は半初期化状態を見てはならない。

以下は決定論的なローカルライフサイクルモデルです。fail_at 回目の取得がリソースの確立前に失敗し、成功したオブジェクトの所有権は呼び出し元に渡されます。終了時は、公開を取消してからリソースを解放します。ホストの failslab を有効にしたり、実際のデバイスの非同期コールバックをシミュレートしたりしません。

class Resources:
    def __init__(self, fail_at=None):
        self.fail_at = fail_at
        self.attempts = 0
        self.live = set()
        self.freed = []
        self.published = None

    def acquire(self, name):
        self.attempts += 1
        if self.attempts == self.fail_at:
            raise MemoryError(name)
        assert name not in self.live
        self.live.add(name)
        return name

    def release(self, name):
        assert self.published is None and name in self.live
        self.live.remove(name)
        self.freed.append(name)

    def create(self):
        acquired = []
        try:
            for name in ("context", "buffer", "registration"):
                acquired.append(self.acquire(name))
        except MemoryError:
            for name in reversed(acquired):
                self.release(name)
            raise
        self.published = tuple(acquired)
        return self.published

    def destroy(self, owned):
        assert self.published is owned
        self.published = None
        for name in reversed(owned):
            self.release(name)

for failure, expected in [(1, []), (2, ["context"]), (3, ["buffer", "context"])]:
    resources = Resources(failure)
    try:
        resources.create()
    except MemoryError:
        pass
    else:
        raise AssertionError("目標失敗未发生")
    assert resources.attempts == failure
    assert not resources.live and resources.published is None
    assert resources.freed == expected

resources = Resources()
owned = resources.create()
assert resources.live == set(owned) and not resources.freed
resources.destroy(owned)
assert not resources.live and resources.published is None
assert resources.freed == ["registration", "buffer", "context"]

ここでは、関数の戻り値の失敗だけでなく、目標注入ポイントに実際に到達したか、後続の取得が継続していないか、リソースがリークまたは重複解放されていないか、未完成のオブジェクトが外部に可視でないかもチェックします。これらがエラーパスの検証可能な不変条件です。

実際のドライバの登録操作では、戻り値の前にワークが開始されるか、外部オブジェクトが参照を保持し続ける可能性があります。公開を取消した後、新しいリクエストの取消、保留中の操作の待機、およびコールバックの終了を待ち、使用されている buffer/context を解放する必要があります。free 文を機械的に逆順に実行するだけでは不十分です。API の確率とフィルタパラメータを注入してパスをトリガーし、リソースと可視性のチェックでそのパスが正しいかどうかを判断します。これらは互いに代替できません。

失敗ポイントを異なるリソースの取得後に配置し、クリーンアップ方法を切り替えます。残りのリソース数だけでなく、依存関係にあるオブジェクトがまだ生存している場合に、それが依存しているオブジェクトを事前に解放していないかもチェックします。

図を準備しています
失敗が異なる位置で発生した場合、クリーンアップ責任はどのように変化するか?

エラーパスでは、取得したリソースのみを解放し、使用依存関係に従って取消す必要があります。最終的なリソース数が0でも、途中でまだ使用されているバッファを先に解放していないことを証明するには不十分です。

納品可能な再現記録

正確なカーネルバージョンまたはコミット、.config、起動パラメータ、モジュールと taint 状態、最小入力、実際の動作、期待される動作、および元のログを記録します。修正が特定の設定下でのみ検証される場合は、その境界を明確にしてください。

これらの証拠を ビルドとモジュール の成果物に接続して初めて、テストが目標コードを実行していることを確認でき、別の同名のカーネルではないことを保証できます。