StackTraceを実装する話

* この記事では、MSVCのinlineアセンブリ構文を使う。
** C++はあまり詳しくないのと、深夜テンションで思いついた実装なので、もっと速い・簡単なやり方があればぜひ教えてほしい。

Unity3Dをやっている人は分かるが、簡単に実例を説明しよう。例えば、描画関数の中でしか使えない関数がある。

void OnDraw () {
    Game::DrawCube(...);
}

ここでGame::DrawCubeはOnDrawおよびOnDrawで呼ばれた関数からの実行出なければいけない。では、エラーチェックとしてOnDrawから呼ばれたかを確認したい。

じゃあ、これをどう実装すればいいのかという話。もちろんStackUnwindとかのライブラリ使ってもいいけど、そもそもデバッグではなくて実際のリリースでも使いたいのと、関数の名前より関数のIDを見るのと、個人的に人の実装を使いたくない癖(ここ重要)がある。

  • StackFrameについて

さて、C++(別にC++じゃなくても)のDisassemblyに着目する。関数が呼ばれたときにstackをみると

address   value
------------------------
EBP-4     return address
EBP       old EBP
EBP+4     funcvar1
EBP+8     funcvar2
...

になっている。これを関数のStackFrameと言う。

では、EBPはこの関数のヘッドを指しているが、その値がなんと呼ばれた関数のヘッドのアドレスが入っている。つまり、

mov EBP [EBP]

をやれば、StackTraceをすることができる(EBPは破壊されるのでこのまま使わないが)。

  • アドレスに着目

関数というのは、あくまで下のようにStackにある変数を操ったりする命令の集合である。例えば、以下ではDrawCubeを呼ぶOnDraw関数の例である。

        OnDraw:
0x123     instruction 1
0x125     instruction 2
...
0x200     call Game::DrawCube (0x502)
0x204     ret

(0x502はGame::DrawCubeのラベルが存在する位置である。例えばOnDrawだと0x123となる)

ここで、Game::DrawCubeが呼ばれた時のStackFrameを見ると、return addressが0x204となる。つまり、この関数が終わったらEIP(instruction pointer)がどこを指せばいいかを教えている。

では、この情報をどう使えばいいかとなるが、まず「呼ばれた関数」の定義を改める。「この関数はあの関数であるか」というのは、厳密に言うと「この関数のreturn addressは一緒なのか」となる。なぜかというと関数の名前より、システムが描画したいときにしか呼ばない「一ヵ所」からの実行だからである。

  • 実装

まず、OnDraw関数を呼ぶところは一定なので、下のようにあらかじめ登録することができる。なお、ラベルはアプリで共通されるため、__COUNTER__などでユニークなラベルにする必要がある。

int funcLoc;
__asm{
    uniquelabel1:
    mov EAX, offset uniquelabel1
    mov funcLoc, EAX
}

なお、このコードは関数を呼んだ直後に入れる。

次に、DrawCubeの中で、親(の親の親の…)のreturn addressがfuncLocであるかを確認すればいい。

偽コード
for EBP=*EBP
while *(EBP+4) != funcLoc ;

実際のコード

__asm{
    mov EBX, EBP
  loop:
    mov EBX [EBX]
    mov EAX, [EBX-4]
    cmp EAX, funcLoc
    jne loop
}
  • Access Violation防止

上のループは、funcLocが見つからないと永遠にループしてAccessViolationエラーが起こる。これを防止するにはどこかで止める必要がある。まず、mainスレッドからの関数と考えれば、
1. assert (std::this_thread::get_id() == mainThreadId);
2. asmのなかに

cmp EAX, mainThreadFuncLoc
je end

ただし、mainThreadFuncLocはmain()を呼んだ__CRT_Startupなんちゃらの場所である。

  • まとめ

アセンブリをいじることで、関数の親をトレースすることができた。この方法を使って、この関数を呼んだ親を区別することも可能となる。なお、デバッガでRTCが入ると、関数が実行された後にEBPの確認instructionが入るので、アドレス登録が動かなくなる場合がある。解決方法考え中(とりあえずオフにした)。もちろんリリースでは影響されない。

Posted on