web-dev-qa-db-ja.com

CまたはC ++で呼び出しスタックを出力

特定の関数が呼び出されるたびに、CまたはC++で実行中のプロセスに呼び出しスタックをダンプする方法はありますか?私が考えているのは次のようなものです:

void foo()
{
   print_stack_trace();

   // foo's body

   return
}

print_stack_traceは、Perlの caller と同様に機能します。

またはこのようなもの:

int main (void)
{
    // will print out debug info every time foo() is called
    register_stack_trace_function(foo); 

    // etc...
}

ここで、register_stack_trace_functionは、fooが呼び出されるたびにスタックトレースが出力されるような内部ブレークポイントを配置します。

このようなものは、いくつかの標準Cライブラリに存在しますか?

GCCを使用してLinuxで作業しています。


バックグラウンド

この動作に影響を与えないいくつかのコマンドラインスイッチに基づいて異なる動作をするテスト実行があります。私のコードには、これらのスイッチに基づいて異なる方法で呼び出されると思われる擬似乱数ジェネレータがあります。スイッチの各セットでテストを実行し、乱数ジェネレーターがそれぞれ異なる方法で呼び出されるかどうかを確認できるようにします。

95
Nathan Fellman

Linux専用のソリューションの場合は、 backtrace(3) を使用できます。これは、単にvoid *の配列を返します(実際、これらはそれぞれ、対応するスタックフレームからの戻りアドレスを指します)。これらを有用なものに変換するには、 backtrace_symbols(3) があります。

backtrace(3)の注記セクション に注意してください:

シンボル名は、特別なリンカーオプションを使用しないと使用できない場合があります。 GNUリンカーを使用するシステムの場合、-rdynamicリンカーオプションを使用する必要があります。 「静的」関数の名前は公開されておらず、バックトレースでは使用できないことに注意してください。

72
Idan K

スタックトレースを強化

ドキュメント: https://www.boost.org/doc/libs/1_66_0/doc/html/stacktrace/getting_started.html#stacktrace.getting_started.how_to_print_current_call_stack

これは、これまで見てきた中で最も便利なオプションです。

  • 実際に行番号を印刷できます。

    ただし、addr2lineを呼び出すだけです。これはいため、トレースが多すぎる場合は遅くなる可能性があります。

  • デフォルトでデマングル

  • Boostはヘッダーのみであるため、ビルドシステムを変更する必要はほとんどありません

main.cpp

#include <iostream>

#define BOOST_STACKTRACE_USE_ADDR2LINE
#include <boost/stacktrace.hpp>

void my_func_2(void) {
    std::cout << boost::stacktrace::stacktrace() << std::endl;
}

void my_func_1(double f) {
    my_func_2();
}

void my_func_1(int i) {
    my_func_2();
}

int main() {
    my_func_1(1);   /* line 19 */
    my_func_1(2.0); /* line 20 */
}

残念ながら、それは最近追加されたようで、パッケージlibboost-stacktrace-devはUbuntu 16.04には存在せず、18.04にのみ存在します。

Sudo apt-get install libboost-stacktrace-dev
g++ -fno-pie -ggdb3 -O0 -no-pie -o main.out -std=c++11 \
  -Wall -Wextra -pedantic-errors main.cpp -ldl

最後に-ldlを追加する必要があります。そうしないと、コンパイルが失敗します。

次に:

./main.out

与える:

 0# my_func_2() at /root/lkmc/main.cpp:7
 1# my_func_1(int) at /root/lkmc/main.cpp:16
 2# main at /root/lkmc/main.cpp:20
 3# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 4# _start in ./main.out

 0# my_func_2() at /root/lkmc/main.cpp:7
 1# my_func_1(double) at /root/lkmc/main.cpp:12
 2# main at /root/lkmc/main.cpp:21
 3# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 4# _start in ./main.out

-O3で:

 0# my_func_2() at /usr/include/boost/stacktrace/stacktrace.hpp:217
 1# my_func_1(double) at /root/lkmc/main.cpp:11
 2# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 3# _start in ./main.out

 0# my_func_2() at /usr/include/boost/stacktrace/stacktrace.hpp:217
 1# main at /root/lkmc/main.cpp:21
 2# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 3# _start in ./main.out

バックトレースは一般に、最適化によって取り返しのつかないほど破壊されることに注意してください。テールコールの最適化は、その顕著な例です。 テールコールの最適化とは?

出力は、以下の「glibcバックトレース」セクションでさらに説明されていますが、これは類似しています。

Ubuntu 18.04、GCC 7.3.0、ブースト1.65.1でテスト済み。

glibcバックトレース

ドキュメント: https://www.gnu.org/software/libc/manual/html_node/Backtraces.html

main.c

#include <stdio.h>
#include <stdlib.h>

/* Paste this on the file you want to debug. */
#include <stdio.h>
#include <execinfo.h>
void print_trace(void) {
    char **strings;
    size_t i, size;
    enum Constexpr { MAX_SIZE = 1024 };
    void *array[MAX_SIZE];
    size = backtrace(array, MAX_SIZE);
    strings = backtrace_symbols(array, size);
    for (i = 0; i < size; i++)
        printf("%s\n", strings[i]);
    puts("");
    free(strings);
}

void my_func_3(void) {
    print_trace();
}

void my_func_2(void) {
    my_func_3();
}

void my_func_1(void) {
    my_func_3();
}

int main(void) {
    my_func_1(); /* line 33 */
    my_func_2(); /* line 34 */
    return 0;
}

コンパイル:

gcc -fno-pie -ggdb3 -O3 -no-pie -o main.out -rdynamic -std=c99 \
  -Wall -Wextra -pedantic-errors main.c

-rdynamicは重要な必須オプションです。

実行:

./main.out

出力:

./main.out(print_trace+0x2d) [0x400a3d]
./main.out(main+0x9) [0x4008f9]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f35a5aad830]
./main.out(_start+0x29) [0x400939]

./main.out(print_trace+0x2d) [0x400a3d]
./main.out(main+0xe) [0x4008fe]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f35a5aad830]
./main.out(_start+0x29) [0x400939]

そのため、インライン最適化が行われ、一部の機能がトレースから失われたことがすぐにわかります。

アドレスを取得しようとすると:

addr2line -e main.out 0x4008f9 0x4008fe

私達は手に入れました:

/home/ciro/main.c:21
/home/ciro/main.c:36

完全にオフです。

代わりに-O0で同じことを行うと、./main.outは正しい完全なトレースを提供します。

./main.out(print_trace+0x2e) [0x4009a4]
./main.out(my_func_3+0x9) [0x400a50]
./main.out(my_func_1+0x9) [0x400a68]
./main.out(main+0x9) [0x400a74]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f4711677830]
./main.out(_start+0x29) [0x4008a9]

./main.out(print_trace+0x2e) [0x4009a4]
./main.out(my_func_3+0x9) [0x400a50]
./main.out(my_func_2+0x9) [0x400a5c]
./main.out(main+0xe) [0x400a79]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f4711677830]
./main.out(_start+0x29) [0x4008a9]

その後:

addr2line -e main.out 0x400a74 0x400a79

与える:

/home/cirsan01/test/main.c:34
/home/cirsan01/test/main.c:35

行が1つだけ外れているのはなぜですか?しかし、これはまだ使用可能かもしれません。

結論:バックトレースは、-O0でのみ完全に表示される可能性があります。最適化により、元のバックトレースはコンパイルされたコードで根本的に変更されます。

Ubuntu 16.04、GCC 6.4.0、libc 2.23でテスト済み。

glibc backtrace_symbols_fd

このヘルパーはbacktrace_symbolsよりも少し便利で、基本的に同じ出力を生成します。

/* Paste this on the file you want to debug. */
#include <execinfo.h>
#include <stdio.h>
#include <unistd.h>
void print_trace(void) {
    size_t i, size;
    enum Constexpr { MAX_SIZE = 1024 };
    void *array[MAX_SIZE];
    size = backtrace(array, MAX_SIZE);
    backtrace_symbols_fd(array, size, STDOUT_FILENO);
    puts("");
}

Ubuntu 16.04、GCC 6.4.0、libc 2.23でテスト済み。

libunwind

TODOにはglibcバックトレースよりも利点がありますか?非常によく似た出力で、ビルドコマンドを変更する必要がありますが、あまり広く利用できません。

適応コード: https://eli.thegreenplace.net/2015/programmatic-access-to-the-call-stack-in-c/

main.c

/* This must be on top. */
#define _XOPEN_SOURCE 700

#include <stdio.h>
#include <stdlib.h>

/* Paste this on the file you want to debug. */
#define UNW_LOCAL_ONLY
#include <libunwind.h>
#include <stdio.h>
void print_trace() {
    char sym[256];
    unw_context_t context;
    unw_cursor_t cursor;
    unw_getcontext(&context);
    unw_init_local(&cursor, &context);
    while (unw_step(&cursor) > 0) {
        unw_Word_t offset, pc;
        unw_get_reg(&cursor, UNW_REG_IP, &pc);
        if (pc == 0) {
            break;
        }
        printf("0x%lx:", pc);
        if (unw_get_proc_name(&cursor, sym, sizeof(sym), &offset) == 0) {
            printf(" (%s+0x%lx)\n", sym, offset);
        } else {
            printf(" -- error: unable to obtain symbol name for this frame\n");
        }
    }
    puts("");
}

void my_func_3(void) {
    print_trace();
}

void my_func_2(void) {
    my_func_3();
}

void my_func_1(void) {
    my_func_3();
}

int main(void) {
    my_func_1(); /* line 46 */
    my_func_2(); /* line 47 */
    return 0;
}

コンパイルして実行:

Sudo apt-get install libunwind-dev
gcc -fno-pie -ggdb3 -O3 -no-pie -o main.out -std=c99 \
  -Wall -Wextra -pedantic-errors main.c -lunwind

#define _XOPEN_SOURCE 700を先頭にするか、-std=gnu99を使用する必要があります。

実行:

./main.out

出力:

0x4007db: (main+0xb)
0x7f4ff50aa830: (__libc_start_main+0xf0)
0x400819: (_start+0x29)

0x4007e2: (main+0x12)
0x7f4ff50aa830: (__libc_start_main+0xf0)
0x400819: (_start+0x29)

そして:

addr2line -e main.out 0x4007db 0x4007e2

与える:

/home/ciro/main.c:34
/home/ciro/main.c:49

-O0を使用:

0x4009cf: (my_func_3+0xe)
0x4009e7: (my_func_1+0x9)
0x4009f3: (main+0x9)
0x7f7b84ad7830: (__libc_start_main+0xf0)
0x4007d9: (_start+0x29)

0x4009cf: (my_func_3+0xe)
0x4009db: (my_func_2+0x9)
0x4009f8: (main+0xe)
0x7f7b84ad7830: (__libc_start_main+0xf0)
0x4007d9: (_start+0x29)

そして:

addr2line -e main.out 0x4009f3 0x4009f8

与える:

/home/ciro/main.c:47
/home/ciro/main.c:48

Ubuntu 16.04、GCC 6.4.0、libunwind 1.1でテスト済み。

C++デマングリング

Glibc backtraceとlibunwindの両方に対してabi::__cxa_demangleで実行できます。次の例を参照してください。 https://eli.thegreenplace.net/2015/programmatic-access-to-the-call -stack-in-c /

Linuxカーネル

Linuxカーネル内で現在のスレッドスタックトレースを出力する方法?

こちらもご覧ください

特定の関数が呼び出されるたびに、CまたはC++で実行中のプロセスに呼び出しスタックをダンプする方法はありますか?

特定の関数でreturnステートメントの代わりにマクロ関数を使用できます。

たとえば、returnを使用する代わりに、

int foo(...)
{
    if (error happened)
        return -1;

    ... do something ...

    return 0
}

マクロ関数を使用できます。

#include "c-callstack.h"

int foo(...)
{
    if (error happened)
        NL_RETURN(-1);

    ... do something ...

    NL_RETURN(0);
}

関数でエラーが発生するたびに、次のようなJavaスタイルの呼び出しスタックが表示されます。

Error(code:-1) at : so_topless_ranking_server (sample.c:23)
Error(code:-1) at : nanolat_database (sample.c:31)
Error(code:-1) at : nanolat_message_queue (sample.c:39)
Error(code:-1) at : main (sample.c:47)

完全なソースコードはこちらから入手できます。

https://github.com/Nanolatのc-callstack

標準化された方法はありません。 Windowsの場合、機能は DbgHelp ライブラリで提供されます

5
Paul Michalik

古いスレッドに対する別の答え。

これを行う必要があるときは、通常system()pstackを使用します

だからこのようなもの:

#include <sys/types.h>
#include <unistd.h>
#include <string>
#include <sstream>
#include <cstdlib>

void f()
{
    pid_t myPid = getpid();
    std::string pstackCommand = "pstack ";
    std::stringstream ss;
    ss << myPid;
    pstackCommand += ss.str();
    system(pstackCommand.c_str());
}

void g()
{
   f();
}


void h()
{
   g();
}

int main()
{
   h();
}

この出力

#0  0x00002aaaab62d61e in waitpid () from /lib64/libc.so.6
#1  0x00002aaaab5bf609 in do_system () from /lib64/libc.so.6
#2  0x0000000000400c3c in f() ()
#3  0x0000000000400cc5 in g() ()
#4  0x0000000000400cd1 in h() ()
#5  0x0000000000400cdd in main ()

これは、Linux、FreeBSD、およびSolarisで動作するはずです。 macOSにはpstackや単純な同等物はないと思いますが、これは スレッドには代替手段があるようです です。

4
Paul Floyd

これには Poppy を使用できます。通常、クラッシュ中にスタックトレースを収集するために使用されますが、実行中のプログラムに対しても出力できます。

ここからが良い部分です。スタック上の各関数の実際のパラメーター値、さらにローカル変数、ループカウンターなどを出力できます。

2
Orlin Georgiev

自分で機能を実装できます。

グローバル(文字列)スタックを使用し、各関数の開始時に、関数名とそのような他の値(パラメーターなど)をこのスタックにプッシュします。関数の終了時に再度ポップします。

呼び出されたときにスタックの内容を出力する関数を作成し、コールスタックを表示する関数でこれを使用します。

これは多くの作業のように聞こえるかもしれませんが、非常に便利です。

2
slashmais

Boostライブラリを使用して、現在のコールスタックを印刷できます。

#include <boost/stacktrace.hpp>

// ... somewhere inside the `bar(int)` function that is called recursively:
std::cout << boost::stacktrace::stacktrace();

ここの男: https://www.boost.org/doc/libs/1_65_1/doc/html/stacktrace.html

2
Barkles

もちろん次の質問は、これで十分でしょうか?

スタックトレースの主な欠点は、正確な関数が呼び出されるのに、引数の値など、デバッグに非常に役立つ他のものがないことです。

Gccとgdbにアクセスできる場合は、assertを使用して特定の条件を確認し、満たされていない場合はメモリダンプを生成することをお勧めします。もちろん、これはプロセスが停止することを意味しますが、単なるスタックトレースではなく、本格的なレポートが得られます。

目立たない方法を希望する場合は、常にロギングを使用できます。 Pantheios など、非常に効率的なロギング機能があります。これにより、現在の状況をより正確に把握できます。

2
Matthieu M.

私はこのスレッドが古いことを知っていますが、他の人にとっては役に立つと思います。 gccを使用している場合、その機器機能(-finstrument-functionsオプション)を使用して、関数呼び出し(エントリおよび終了)を記録できます。詳細については、こちらをご覧ください: http://hacktalks.blogspot.fr/2013/08/gcc-instrument-functions.html

したがって、たとえば、すべての呼び出しをスタックにプッシュしてポップすることができ、それを印刷したいときは、スタックにあるものを見るだけです。

私はそれをテストしました、それは完全に動作し、非常に便利です

更新:GCCドキュメントのインストルメンテーションオプションに関する-finstrument-functionsコンパイルオプションに関する情報も見つけることができます。 https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html

1
François

GNUプロファイラーを使用できます。コールグラフも表示されます!コマンドはgprofであり、オプションを使用してコードをコンパイルする必要があります。

0
Saurabh Shah