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: 2017年11月7日, by : chokopan
1 thought on “StackTraceを実装する話”
Comments are closed.