バックトレースに行った方法

少し前まで、当社では、ソフトウェアで発生したエラーに関する通知をユーザーが送信できるようにすることを決定しました。 すぐに言ってやった。 しかし、ここでは、実行時にプログラム呼び出しの現在のスタックのバックトレースを直接取得するという問題が発生しました。 この問題を解決する方法はいくつかあることが判明しました。 この記事は、C / C ++で記述され、LinuxおよびFreeBSDで実行されるプログラムのバックトラッキングの問題に関する私の調査の結果です。





理論のビット



原則として、呼び出しチェーンの取得は非常に簡単です。 必要な情報はすべてプログラムスタックに保存されます。 関数を呼び出すための最新のコンパイラは、いわゆるスタックフレームを形成します 。 各フレームの先頭には、前のフレームのアドレスがあります。 そして、フレームの直前に、戻りアドレスが保存されます。 関数が完了した後、次に実行される命令のアドレス。 したがって、行う必要があるのは、フレームのリストを調べて戻りアドレスを出力することだけです。

たとえば、これは次のように実行できます(amd64の例):

void * GetReturnAddress(int depth) {    void *res;    asm (        "mov %1, %%rcx\n"        "MOVE: mov 0x0(%%rbp), %%rax\n"        "loop MOVE\n"       "mov 0x8(%%rax), %rax\n"        "mov %%rax, %0\n" : "=m" (res) : "g" (depth) : "rax", "rcx");    return res; }
      
      





関数はさらに短く書くことができます。なぜなら 戻り値はraxレジスタに格納されます。res変数なしで実行できます。

しかし、個人的には、アセンブラーを挿入することはジェダイの本当の方法ではありません。 そこで、別の解決策を探しました。



GCC拡張



私が最初に出会ったのは、gccの作成者から親切に提供された__builtin_return_address関数です。 彼女の完全な説明からの抜粋は次のとおりです。

void * __builtin_return_address(unsigned intレベル) -関数の戻りアドレスを返します。 レベル= 0の場合、関数は現在の関数の戻りアドレスを返し、レベル= 1の場合 、現在の関数を呼び出した関数の戻りアドレスなどを返します。

使用する場合、たった1つのことがあります:コンパイル時、関数はアセンブラーコードの行に展開されます(スタックに沿って行けば進むほど、行が増えます)。したがって、変数をパラメーターとして受け入れる方法がわかりません。 したがって、美しいビューレコードの代わりに:

return __builtin_return_address(i);

私はugいを書かなければなりません:

 switch(level) { case 0: return __builtin_return_address(1); case 1: return __builtin_return_address(2); …. }
      
      





すでに良い。 どうぞ



バックトレース



Linuxでは、標準ライブラリはプログラマに必要な情報を取得するための幅広い機能を提供します。 FreeBSDでは、これらの目的のためにlibexecinfoライブラリをインストールする必要があります。 ここにあります:

int backtrace(void ** buffer、int size) -呼び出しプログラムのバックトレースでバッファを埋める関数。

char ** backtrace_symbols(void * const * buffer、int size) -最初の関数の結果を取得し、関数のアドレスをテキスト表現に変換する関数。

void backtrace_symbols_fd(void * const * buffer、int size、int fd)-mallocを介して行にメモリを割り当てる代わりに、情報をファイルに直接書き込みます。

呼び出しスタックにある各関数について、 backtrace_symbolsは次のような行を返します。



./prog(_Z6myfunci+0x1a)[0x8048840]

ここで、progはバイナリの名前です

_Z6myfunci-コード化された関数名

0x1a-関数内のオフセット

0x8048840-関数アドレス



より詳細な情報と、 man backtraceでの使用例があります。 backtrace_symbolsが正しく機能するためには、 -rdynamicオプションを使用してプログラムをコンパイルする必要があることに注意してください。 これは、backtrace_symbols関数の名前に関する情報がダイナミックリンクテーブルから取得されるためです。 デフォルトでは、動的ライブラリからロードされた関数のみがそこに到達します。 このテーブルにすべての機能を強制的に追加するには、上記のキーも必要です。



dladdr



backtrace_symbols関数の欠点は、その結果をテキストとして表示することです。 つまり たとえば、関数の名前を使用して操作を行う場合は、この行を解析する必要があります。 もうジェダイじゃない! なぜこれが必要なのかは、後ほど明らかになります。

ここでは、 dladdr関数が役立ちます。 実際、自分の中でbacktrace_symbolsを呼び出すのは彼女です。 その署名は非常に単純です-関数アドレスを入力に渡し 、出力でDl_info型の構造を取得します。

int dladdr(void * addr、Dl_info * info);

dladdr呼び出しの結果が成功した場合、構造に backtrace_symbolsの場合と同じデータすべて含まれます

まあ、ほとんど問題ありません。 これで、エンコードされた形式であるにもかかわらず、リターンアドレスと関数名さえあります(この問題の解決については後で説明します)。 まだどの情報を引き出すことができるか見てみましょう。 たぶん、ソースファイルの名前と、関数が配置されている行のアドレスさえですか? 実際、混乱する必要がありますが!



これで何をする?



原則として、すでに持っているデータで十分です。 アドレスがあると、関数呼び出しを生成した行番号をいつでも見つけることができます。 最も簡単な方法は、gdbデバッガーのlistコマンドを使用することです。 デバッグでコンパイルされた同じバージョンのプログラムがある場合、list * <address>には行番号が表示されます。 ただし、ソースコードも近くにある場合は、「ああ、奇跡!」という行が表示されます。

しかし、プログラムの2つのバージョン(デバッグありとなし)を保存するという考え方は、理想に対するジェダイの願望とは一致しないため、私はストリップを勉強することにしました。 彼はバイナリファイルとデバッグ情報を別々に保存する方法を知っていることを長い間知っていました。 それは非常に簡単でした:

  1. 通常どおりデバッグを使用してプログラムをアセンブルします(-gスイッチ、またはファンの場合は-g3-インライン関数とすべての種類のマクロがデバッグに含まれます)。
  2. objcopy --only-keep-debug a.out a.out.symを実行します 。 これで、gdbでの快適な作業に必要な情報はすべてa.out.symファイルにあります。
  3. strip a.outを実行します。 つまり a.outからデバッグを削除します。




できるようになりました:

  1. objcopyコマンド--add-gnu-debuglink = a.out.sym a.outを使用して、a.out.symをa.outに関連付けます。 デバッガーは、バイナリが置かれているフォルダーと同じフォルダーで情報を見つけると、a.out.symから必要な情報をすべて自動的にロードします。
  2. symbol-file a.out.symコマンドを使用して、gdbからa.out.symファイルを手動でダウンロードします。

    これで、ソフトウェアのデバッグ情報を収集できますが、クライアントに提供することはできません。 これは思いやり(デバッグはかなり印象的な量)から、またはセキュリティ上の理由から(ハッカーのリバースエンジニアリングを複雑にします)行うことができます。 しかし、クライアントで何かをデバッグする必要がある場合は、いくつかの欠落している.symファイルで単純にそれを埋めることができます。


ただし、行番号だけでなくソースコード自体も確認したいが、クライアントにアップロードしたくない場合(商用ソフトウェアに対する完全に正当な要望)、gdbserverを使用して、プログラムをリモートでデバッグできます。 これを行うには、次のものが必要です。

  1. クライアント側で、 gdbserver 127.0.0.1:2345 a.outを実行します
  2. さらに、gdbを実行し、コマンドターゲットremote 127.0.0.1:2345を実行します。 この場合、すべてのソースファイルは、コンパイル時に使用されたのと同じパスでアクセスできる必要があります。


マングル/デマングル



最後に、関数名を記述するための形式についていくつか説明します。 一言で言えば、リンカが名前の衝突を解決するには、このような関数名の歪みが必要です。 まあ、またはもっと簡単に、プログラムに同じ名前でパラメーターが異なる(オーバーロードされた)2つの関数がある場合、リンカーはどちらを使用するかを正確に知る必要があります。 このために、コンパイラは特別なアルゴリズムに従って関数の名前をエンコードし、新しい一意の名前を割り当てます。 英語では、このプロセスはマングリングと呼ばれ、逆はデマングリングです。

エンコードされた関数名を元の形式に変換する問題を解決するには、再びgcc-extensionを使用できます。

char * abi :: __ cxa_demangle(const char * mangled_name、char * output_buffer、size_t * length、int * status)

この関数は、エンコードされた関数名とバッファーを入力として受け取り、デコードされた名前を出力に出力します。 その使用例はここにあります



そして最後に



適切な情報を取得する最も面白い方法は、単にgdbに問い合わせることでした。 幸いなことに、後者はこれを行うことを可能にします(関数の例はここから取られます )。

 void print_trace() { char pid_buf[30]; sprintf(pid_buf, "%d", getpid()); char name_buf[512]; name_buf[readlink("/proc/self/exe", name_buf, 511)]=0; int child_pid = fork(); if (!child_pid) { dup2(2,1); // redirect output to stderr fprintf(stdout,"stack trace for %s pid=%s\n",name_buf,pid_buf); execlp("gdb", "gdb", "--batch", "-n", "-ex", "thread", "-ex", "bt", name_buf, pid_buf, NULL); abort(); /* If gdb failed to start */ } else { waitpid(child_pid,NULL,0); } }
      
      





必要なのは、print_trace関数を呼び出すことだけです。できれば、呼び出しスタックは標準出力に出力されます。 原則として、このオプションは機能していますが、非常に遅く、gdbのインストールが必要です。



以上です。

素敵なデバッグをしてください!



All Articles