フレームポインタが最適化されていても、スタックメモリを調べて保存されたリターンアドレスを探すことで、スタックフレームを区別できることがよくあります。x86の関数呼び出しシーケンスは、常に次のもので構成されていることに注意してください。
call someFunc ; pushes return address (instr. following `call`)
...
someFunc:
push EBP ; if framepointer is used
mov EBP, ESP ; if framepointer is used
push <nonvolatile regs>
...
したがって、スタックは常に(フレームポインタが欠落している場合でも)リターンアドレスをそこに持っています。
差出人住所をどのように認識しますか?
- まず、x86では、命令の長さが異なります。つまり、他のポインタ(!)とは異なり、差出人住所は値がずれている傾向があります。統計的に、それらの3/4は4の倍数で終了しません。
ポインタの位置がずれている場合は、差出人住所の候補として適しています。
- 次に、
call
x86の命令には特定のオペコード形式があることに注意してください。リターンアドレスの数バイト前を読み取り、そこにオペコードが見つかるかどうかを確認しますcall
(99%の場合、直接呼び出しの場合は5バイト戻り、レジスタを介した呼び出しの場合は3バイト戻ります)。もしそうなら、あなたは差出人住所を見つけました。
これは、C ++ vtablesとリターンアドレスを区別する方法でもあります。スタック上にあるvtableエントリポイントですが、これらのアドレスから「振り返って」みると、call
指示が見つかりません。
この方法を使用すると、シンボルやフレームサイズのデバッグ情報などがなくても、スタックから呼び出しシーケンスの候補を取得できます。
これらの候補から実際の呼び出しシーケンスをつなぎ合わせる方法の詳細はそれほど単純ではありませんが、逆アセンブラと、最も低い差出人住所から最後の既知のプログラムの場所までの潜在的な呼び出しフローを追跡するためのヒューリスティックが必要です。多分いつか私はそれについてブログを書くでしょう;-)しかしこの時点で私はむしろスタックオーバーフロー投稿のマージンがこれを含むには小さすぎると言いたいです...