ハンドアセンブル入門

ミラクシアエッジテクノロジーの川井です。

組込みソフトの関数テストを行う際、ベアメタル環境など開発環境によるテスト支援がないと異常系テストの実施が難しい場合があります。

例えばテスト対象の関数が呼び出している標準ライブラリのAPIが失敗したケースのカバレッジをとるには、通常はOSやライブラリのデバッグ支援機能を活用してフォールトインジェクションを行いますが、開発環境がそのようなリッチな機能を提供していないとテスト実施が難しくなることがあります。

このような場合に取りうる手段はいくつかありますが、その一つに実行時の関数フックがあります。テスト対象の関数が呼び出している標準ライブラリのAPIの先頭を実行時にブランチ命令に書き換えてフック関数を挿入し、異常を模擬します。他にテスト手順としてデバッガで返り値を書き換える方法やテスト対象の関数自体にテスト用のフォールトインジェクション機能を埋め込む方法がありますが、前者はテスト手順の運用コストが重く、後者はテスト時と出荷時でテスト対象関数のコードが異なってしまう問題があります。標準ライブラリに対して関数フックをかける手法なら完全な自動化が可能で運用コストが低く、またテスト対象コードの編集を避けることができます。

本記事では関数フック自体は扱いませんが、関数フックを実装するために知識として必要になるハンドアセンブルの手法を紹介します。

ハンドアセンブルとは

アセンブラ言語による表現(ニーモニック)を機械語に翻訳するソフトウェアのことをアセンブラと呼びます。アセンブラを使わずに人が手で機械語を書くことをハンドアセンブルと呼びます。ハンドアセンブルの手法を学ぶことで、実行時にテキストセクションを書き換える自己編集プログラムを作ることができるようになります。

ARMv7-M命令セット

本記事ではミラクシアで比較的ユーザーの多いARMv7-M命令セットを扱います。

ARM CPUの場合、アセンブラニーモニックと機械語表現のリファレンスはArchitecture Reference Manualという文書に記載されています。ARMv7-Mの場合は以下に示すURLのARMv7-M Architecture Reference Manualです。

mov命令をハンドアセンブルする

本記事では機械語を手書きする際に参照すべき文書を示し、読者が手法を再現できるようになることを目的としているため、書きやすく簡単な命令を例に説明します。

ここではニーモニックmov r3, #0x5aをハンドアセンブルすることを考えます。前節で示したARMv7-M Architecture Reference ManualのA7.7.76 MOV (immediate)セクションを見ると機械語表現のフォーマットが掲載されています。

3パターンの表現がありますが、8ビットの範囲なら16ビット命令のEncoding T1のフォーマットで記述できるので、これを採用します。

2進数表現の図にあるRdのところにmov r3, #0x5aの第一オペランドr3のレジスタ番号、imm8のところに第二オペランドを即値として埋めます。つまりRd=3, imm8=0x5aです。

Rd=3, imm8=0x5aとして、mov r3, #0x5aは機械語表現で0b0010001101011010=0x235aと記述できることが分かりました。

デモ

前節で作った機械語表現0x235aが正しいのか、GCCとQEMUを使ってインラインアセンブラで確認します。

#include <stdio.h>

int main() {
    int r3val;

    // 通常のアセンブラ表現のテスト
    // いったんr3に0x55を入れた後、C言語側のr3valローカル変数に取り出し
    asm volatile (
        "mov r3, #0x55\n"
        "mov %0, r3\n"
        : "=r"(r3val)
        :
        : "r3"
    );

    // r3=0x55と表示される
    // ここでローカル変数r3valがどのCPUレジスタに割り当てられるか分からない
    printf("r3=0x%02x\n", r3val);

    // 機械語を直接記述する表現のテスト
    // いったんr3に0x5aを入れた後、C言語側のr3valローカル変数に取り出し
    asm volatile (
        ".short 0x235a\n"
        "mov %0, r3\n"
        : "=r"(r3val)
        :
        : "r3"
    );

    // r3=0x5aと表示される
    // ここでローカル変数r3valがどのCPUレジスタに割り当てられるか分からない
    printf("r3=0x%02x\n", r3val);
    return 0;
}

GCCの場合、.shortで16ビット整数をその場に記述できるため、関数内でasm volatileとして.short 0x235aとすることで機械語表現をその場に直接記述できます。

コンパイル・実行してみましょう。

$ arm-linux-gnueabi-gcc -static -mthumb -o test test.c
$ qemu-arm test
r3=0x55
r3=0x5a

期待通り動作しました。

arm-linux-gnueabi-objdump -D testとしてコンパイル後の命令列を見てみます。

.short 0x235aと書いた部分がそのまま命令として記述できていることが分かります。

まとめ・応用

本記事では単にmov r3, #0x5aと書けばよいところ、わざわざ.short 0x235aと分かりにくく書いてみてコンパイラによる機械語生成と同様の手順を手動で実行できることを示しました。

本記事で紹介した手順は単体では意味がありませんが、手順をアルゴリズムとして実装することで、実行時に自分自身のテキストセクションを書き換えるようなプログラムを作ることができるようになります。有力な応用として関数フックと呼ばれる手法があり、例えばテストの際に標準ライブラリの関数にフックをかけて強制的に標準ライブラリのAPI実行を失敗させることで、テスト対象プログラムの異常系テストを非破壊で実施できるなどの用途があります。

採用情報

ミラクシアでは一緒に組込みソフト開発をする仲間を募集しています。

参考文献