何のことはない、自分の書いたコードがスタックを破壊していたという、CやC++ではありがちなパターンに悩まされたという、ただそれだけ。
慣れないVisual C++で開発中に、'''Run-Time Check Failure #0'''なるエラーメッセージに直面した。
Run-Time Check Failure #0 - The value of ESP was no properly saved across a function call. This is usually a result of calling a function declared with one calling convention with a function pointer declared with a defferent calling convention.
どうやら関数呼び出しから戻ってくる時にレジスタESPの値をうまく復元できなかったらしい。
このエラーでよくあるのは関数の呼び出し規約が異なる場合で、例えばstdcallで呼ぶべき関数をcdecl呼び出しの関数ポインタに突っ込んでポインタ経由で実行しようとした場合に起きるらしい。
「らしい」というのは、その情報をネットで見かけたことはあるけど、自分自身では経験したことがないから。
今回このエラーがでた原因は「スタックのデータを自分で壊していた」という非常にありがちなことだったのだが、解決までに1日費やしてしまったorz
ちなみに解決までの流れはこんな感じだった。
- 事前知識など。
- 「関数呼び出し時、呼び出し前にレジスタの値をスタックに積んでおき、呼出し後にスタックから取り出して復元する」という程度の知識はあった。x86のレジスタやアセンブラの知識は全く無かった。というかアセンブラなんて基本情報技術者のCASL2を齧ったぐらいだ。CASLは基本的にニセ?アセンブラだもんなあ。
- 取り敢えず勘で調査範囲を絞り込んだ。
- 問題の発生箇所がほぼ固定されていて、且つ関数呼び出し規約なんて全く関係しそうにない場所だったので、誰か(何れかのスレッド)がスタックを壊している可能性を疑ってみることにした。
- ネットでESPについて調べた。
- どのレジスタがどんな役割を持っているのか分からなくては話にならないので……。ついでにcdeclでの関数呼び出し時のESPの退避、復元の具体的な手順を知りたかったのだが、うまい具合に情報を見つけられなかった。なので、
- 現物からESP退避、復元に関係しそうな部分を探した。
- レジスタとメモリダンプを眺めながら関数呼び出しの前後をステップ実行して、関数呼び出しから戻るのに必要そうな情報を大体どの辺りに保持しているのか調べた。もっとも「大体この辺りのどれか」程度にしか分からなかったけど。
- データ破壊の瞬間を探した。
- エラーが起きる場所がほぼ同じだったので、その少し手前からメモリダンプを眺めつつステップ実行して、変更したらいけなそうな場所を書き換える瞬間を探した。100回以上ループしてる部分だったこともあり、かなり時間がかかった。
- データ破壊を引き起こしていたステートメントとその前後を眺めた。
- 十中八九、自分がミスしているんだという信念? をこめて。
……何というか、非常に力技だった。
ちなみにデータ破壊していた部分は二重ループ部分で、外のループの添字がi、中のループの添字がjだったのだが、jを使うべきなのに誤ってiを使っていた部分が1ヶ所あった。
jは配列(内部変数)の走査に使用していて、且つiとjの最大値はmax(i) > max(j)だった為、配列の範囲外へのアクセス(代入)が発生した。その場所が偶々ESPの復元に関連してそうな場所だったようだ。
1日かけてあぶり出した結果が「iとjの書き間違い」だなんてorz