読者です 読者をやめる 読者になる 読者になる

DynamicPatcher - Runtime C++ Editing


[2013/12/25 追記] この DynamicPatcher をさらに改良し、Visual Studio のアドインとして実装した Alcantarea をリリースしました

DynamicObjLoader を改良し、実用に耐えそうな実行時 C++ コード編集機能を実装しました。
C++ コードを編集してそれをリアルタイムに反映させることができます。

DynamicPatcher
https://github.com/i-saint/DynamicPatcher (bin)


DynamicObjLoader の時点で一応は同等機能を実現していたのですが、更新したい関数を事前にマクロで包む必要があったり、virtual 関数を持つ class はシリアライズが必要だったりと、運用上色々面倒な点がありました。
今回はそのへんが大きく改良されています。
・前準備なしに既存のほぼ全ての関数を差し替えられるようになりました。CRT や WinAPI の関数でも差し替え可能です。
・virtual 関数を持つ class でもシリアライズなしで実行を継続可能になりました。(データ構造が変わる変更だとさすがに無理で、シリアライズするとか事前に余白を設けておくとかの対処が必要です)
・.obj .lib に加え、.dll もロードできるようになりました。.obj だとデバッガでソースを追えませんが、.dll だと可能なため、お手軽に編集したい場合 .obj、ちゃんとデバッグしたい場合 .dll 化、という使い分けができます。(.obj のままデバッグできれば理想的で、実現できないか調査中ですが望み薄です…)


編集した C++ コードをその場でコンパイルし、できた .obj (or .lib .dll) を自力でロード&リンクして中の関数を使えるようにし、既存の関数を差し替える、という大まかな仕組みは以前と同じです。
今回大きく変わったのは、関数を差し替える処理です。以前は差し替え可能にしたい関数を関数ポインタで間接参照させていましたが、今回は関数の中身を直接書き換えるようにしています。
具体的には、関数の先頭 5 byte を新しい関数への jmp に書き換えます。元の 5 byte 分を含むコードは別の領域に移し、末尾に元の場所への jmp を加えておきます。(==これを call すれば更新前の関数が実行される)
この方法だと関数のアドレスは変わらないので関数ポインタによる間接参照は必要なくなり、virtual 関数がある class でも vftable の更新が必要なくなります。

また、この DynamicPatcher を dll injection で既存のプロセスに強引に仕込むツールも用意しています。
ソースの更新を監視するディレクトリ、ビルドコマンド、ロードするファイルなどは設定ファイルで外部から編集できるため、全く前準備していない既存のプロセスを実行時編集することも可能です。
(ただこの場合、最初にロードするモジュールで dpUpdate() を定期的に呼ばせる処理をどこかにねじ込む必要があります。そうしないと更新を反映できません)


単純な使用例:

#include <windows.h> // Sleep()
#include <cstdio>
#include "DynamicPatcher.h"

// dpPatch をつけておくとロード時に同名の関数を自動的に更新する。
// (dpPatch は単なる dllexport。.obj に情報が残るいい指定方法が他に見当たらなかったので仕方なく…。
//  この自動ロードは dpInitialize() のオプションで切ることも可能)
dpPatch void MaybeOverridden()
{
    // ここを書き換えるとリアルタイムに挙動が変わる
    puts("MaybeOverridden()\n");
}

// CRT の関数を差し替える例。今回の犠牲者は puts()
int puts_hook(const char *s)
{
    typedef int (*puts_t)(const char *s);
    puts_t orig_puts = (puts_t)dpGetUnpatched(&puts); // hook 前の関数を取得
    orig_puts("puts_hook()");
    return orig_puts(s);
}

// dpOnLoad() の中身はロード時に自動的に実行される。アンロード時に実行される dpOnUnload() も記述可能
dpOnLoad(
    // dpPatch による自動差し替えを使わない場合、on load 時とかに手動で差し替える必要がある。
    // 元関数と新しい関数の名前が違うケースでは手動指定するしかない。
    dpPatchAddressToAddress(&puts, &puts_hook); // puts() を puts_hook() に差し替える
)

int main()
{
    dpInitialize();
    dpAddSourcePath("."); // このディレクトリのファイルに変更があったらビルドコマンドを呼ぶ
    dpAddModulePath("example.obj"); // ビルドが終わったらこのファイルをロードする
    // cl.exe でコンパイル。msbuild や任意のコマンドも指定可能。
    // 実運用の際は msbuild を使うか、自動ビルドは使用せすユーザー制御になると思われる。
    // (dpStartAutoBuild() を呼ばなかった場合、dpUpdate() はロード済み or module path にあるモジュールに更新があればそれをリロードする)
    dpAddCLBuildCommand("example.cpp /c /Zi");
    dpStartAutoBuild(); // 自動ビルド開始

    for(;;) {
        MaybeOverridden();
        ::Sleep(2000);
        dpUpdate();
    }

    dpFinalize();
}

// cl /Zi example.cpp && ./example


課題。
.obj と .lib の場合、DynamicObjLoader で言及した制限がついてきます。キツいのは RTTI が有効だと vftable の構造が変わるのと、デバッガでソースを追えないのと、リンク時コード生成を有効にしてコンパイルされたものはロードできないあたりでしょうか。
また、リンクの際デバッグ API でシンボルの情報を引くのに初回はえらい時間がかかるようです。規模が大きいプログラムだと数十秒止まったりします。(2 回目以降は速い)
さしあたって .dll ならこれらの制限は回避できます。
また、x64 も対応してるつもりですが、デバッグが不十分なのでおかしなことが起きるかもしれません。追々改良していきます。
あとは x86(64) であれば Linux 系 OS でも同じ事ができるはずなのでいずれ対応したいところです。(Linux 対応までできれば Runtime-Compiled C++ に完勝と言っても過言ではないでしょう!)


実装に際し、Mhook が関数 hook のいい参考になりました。
PE/COFF のファイルフォーマット資料 はロード&リンク処理の細かい仕様を詰めるのにとても役立ちました。