のような擬似コードをいえば、あなたは、スタックのすべてのスタックフレームは、あなたが表現できる可変サイズのデータ構造であり、「パックスタックフレームの配列を」呼び出すことができます。
template struct stackframe<N> {
uintptr_t contents[N];
#ifndef OMIT_FRAME_POINTER
struct stackframe<> *nextfp;
#endif
void *retaddr;
};
問題がおきていることです機能が異なる<N>
- フレームサイズが異なります。
コンパイラはフレームサイズを知っていて、デバッグ情報を作成すると、通常、これらをフレームの一部として出力します。デバッガが最後に行う必要があるのは、最後のプログラムカウンタを見つけて、シンボルテーブルの関数を調べ、その名前を使ってデバッグ情報のフレームサイズを調べることです。これをスタックポインタに追加すると、次のフレームの先頭に移動します。
この方法を使用する場合、フレームリンケージは不要で、を使用してもバックトレースは正常に機能します。一方、フレームリンケージがある場合、新しいスタックフレーム内のすべてのフレームポインタは、関数プロローグコードによって前のものを指すように初期化されるため、スタックを反復するのはリンクされたリストの直後です。
フレームサイズ情報もフレームポインタもありませんが、シンボルテーブルがない場合は、リバースエンジニアリングによってバックトレースを実行して、実際のバイナリからフレームサイズを計算することもできます。プログラムカウンタから始め、シンボルテーブルにある関数を調べてから、関数を最初から逆アセンブルします。関数の先頭と実際にスタックポインタを変更するプログラムカウンタ(スタックに何かを書き込む、スタックスタックを割り当てる)の間で、すべての演算を分離します。これは現在の関数のフレームサイズを計算してスタックポインタから減算し、関数が入力される前にスタックに書き込まれた最後の単語を(ほとんどのアーキテクチャで)見つける必要があります。通常は呼び出し元への戻りアドレスです。必要に応じて繰り返します。
最後に、あなたがスタックの内容のヒューリスティック分析を行うことができます - すべてのプロセスのアドレス空間の実行可能にマッピングされたセグメント内にあるスタック内の単語(と、それによって、戻りアドレス別名機能オフセットすることができる)に分離し、メモリを検索して命令を逆アセンブルして実際にソートの呼び出し命令であるかどうかを確認し、そうであればそれが本当に「次」と呼ばれているかどうか、そして中断のない呼び出しシーケンスを構築できるかどうかを確認します。バイナリが完全に削除されたとしても、これはある程度は機能します(ただし、その場合はすべてリターンアドレスのリストです)。私はGDBがこの手法を採用しているとは思っていませんが、埋め込まれた低レベルのデバッガの中にはいくつかあります。 x86では、命令の長さが異なるため、命令ストリームを介して簡単に「ステップバック」できず、命令の長さが固定されているRISCでは非常に困難です。 ARMでは、これははるかに簡単です。
テール再帰関数、インラインコードなどのように、これらのアルゴリズムの単純な実装または複雑な/徹底的な実装が時々失敗する穴があります。GDBのソースコードは、あなたにいくつかのより多くのアイデアを与えるかもしれない:
http://sourceware.org/cgi-bin/cvsweb.cgi/src/gdb/frame.c?rev=1.287&content-type=text/x-cvsweb-markup&cvsroot=src
GDBは、このような様々な技術を採用しています。
http://en.wikipedia.org/wiki/Call_stack、コンパイラはデバッグ情報マッピングコードのオフセットをソース行に追加します。 –
そのウィキペディアの記事では、他のアルゴリズムの詳細については、「別の関数を呼び出すときにスタックにRBPを格納する」以外の方法はありません。たとえば、コンパイル時に-fomit-frame-pointerが設定されていると、そのアルゴリズムはもう動作しません。 スタックアンワインド記述子はどうですか?スタックトレースをどのように再構築するのに使われますか?他のアルゴリズムはありますか? –
'-fomit-frame-pointer'を指定すると、GDBが起動します。 –