C / C ++プログラムで現在の関数呼び出しスタックを出力する

数日前、同僚によって追跡されたプログラムが不可解に終了し、コアダンプはありませんでした(もちろん、ulimitがオンになりました)。通常の状況では、プログラムが何らかの異常な状態で終了した場合はコアダンプを生成し、プログラムが正常に終了した場合は、exit()関連関数を直接または間接的に呼び出す必要があることがわかっています。この事実に基づいて、プログラムの開始時にシステムによって提供されるatexit()を介してシステムにコールバック関数を登録する方法を考えました。プログラムがexit()を呼び出して終了すると、このコールバック関数が呼び出されます。次に、コールバック関数で現在の関数呼び出しスタックを出力します。これにより、exit()が呼び出される場所を知ることができ、上記の問題を解決できます。上記の方法は、同様の問題を解決するのに非常に効果的です。上記で「コールバック関数に現在の関数呼び出しスタックを出力する」と述べましたが、注意深い友人はこれに気付くはずです。この記事の主な内容は、プログラムで現在の関数呼び出しスタックを印刷する方法を詳しく紹介することです。 。
      「C / C ++プログラムのデバッグに関するいくつかの機能の紹介」というタイトルの記事を書きました。この記事は前の記事に基づいているため、ここを読んで、最初に前の記事を読んでください。これを実現するために、2つの関数backtrace()とbacktrace_symbols()を使用しました。以下は簡単な例です。この例では、特定のメソッドを紹介します。

1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43
#include <execinfo .h> 
#include <stdio .h> 
#include <stdlib .h> 
 
void fun1(); 
void fun2(); 
void fun3(); 
 
void print_stacktrace(); 
 
int main()
{ 
    fun3(); 
} 
 
void fun1()
{ 
    printf( "stackstrace begin:\ n"); 
    print_stacktrace(); 
} 
 
void fun2()
{ 
    fun1(); 
} 
 
void fun3()
{ 
    fun2(); 
} 
 
void print_stacktrace()
{ 
    int size = 16; 
    void * array [16]; 
    int stack_num = backtrace(array、size); 
    char ** stacktrace = backtrace_symbols(array、stack_num);
    for(int i = 0; i <stack_num; ++ i)
    { 
        printf( "%s \ n"、stacktrace [i]); 
    } 
    free(stacktrace); 
}

(注:次の紹介では、ubuntu 11.04、x86_64、gcc-4.5.2を使用しています)

  • 1.次の方法でコンパイルして実行します。
1 
2 
3 
4 
5 
6 
7 
8 
9 
10
wuzesheng @ ubuntu:〜/ work / test $ gcc test.cc -o test1 
wuzesheng @ ubuntu:〜/ work / test $ ./test1 
stackstrace begin:
./ test1()[0x400645] 
./test1()[0x400607] 
。 / test1()[0x400612] 
./test1()[0x40061d] 
./test1()[0x4005ed] 
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xff)[0x7f5c59a91eff] 
./test1() [0x400529]

      上記の実行結果から、関数の呼び出しスタックがわかりますが、それらはすべて16進アドレスであり、少し不快です。もちろん、逆アセンブルで各アドレスに対応する関数を取得することはできますが、それでも少し面倒です。心配しないでください。ゆっくり話して、ステップ2を見てみましょう。

  • 2.次の方法でコンパイルして実行します。
1 
2 
3 
4 
5 
6 
7 
8 
9 
10
wuzesheng@ubuntu:~/work/test$ gcc test.cc -rdynamic -o test2
wuzesheng@ubuntu:~/work/test$ ./test2
stackstrace begin:
./test2(_Z16print_stacktracev+0x26) [0x4008e5]
./test2(_Z4fun1v+0x13) [0x4008a7]
./test2(_Z4fun2v+0x9) [0x4008b2]
./test2(_Z4fun3v+0x9) [0x4008bd]
./test2(main+0x9) [0x40088d]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xff) [0x7f9370186eff]
./test2() [0x4007c9]

      这下终于可以看到函数的名字了,对比一下2和1的编译过程,2比1多了一个-rdynamic的选项,让我们来看看这个选项是干什么的(来自gcc mannual的说明):

1
2
-rdynamic
           Pass the flag -export-dynamic to the ELF linker, on targets that support it. This instructs the linker to add all symbols, not only used ones, to the dynamic symbol table. This option is needed for some uses of "dlopen" or to allow obtaining backtraces from within a program.

      从上面的说明可以看出,它的主要作用是让链接器把所有的符号都加入到动态符号表中,这下明白了吧。不过这里还有一个问题,这里的函数名都是mangle过的,需要demangle才能看到原始的函数。关于c++的mangle/demangle机制,不了解的朋友可以在搜索引擎上搜一下,我这里就不多就介绍了。这里介绍如何用命令来demangle,通过c++filt命令便可以:

1
2
wuzesheng@ubuntu:~/work/test$ c++filt < << "_Z16print_stacktracev"
print_stacktrace()

      写到这里,大部分工作就ok了。不过不知道大家有没有想过这样一个问题,同一个函数可以在代码中多个地方调用,如果我们只是知道函数,而不知道在哪里调用的,有时候还是不够方便,bingo,这个也是有办法的,可以通过address2line命令来完成,我们用第2步中编译出来的test2来做实验(address2line的-f选项可以打出函数名, -C选项也可以demangle):

1
2
3
4
wuzesheng@ubuntu:~/work/test$ addr2line -a 0x4008a7 -e test2 -f
0x00000000004008a7
_Z4fun1v
??:0

      Oh no,怎么打出来的位置信息是乱码呢?不急,且看我们的第3步。

  • 3. 通过下面的方式编译运行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
wuzesheng@ubuntu:~/work/test$ gcc test.cc -rdynamic -g -o test3
wuzesheng@ubuntu:~/work/test$ ./test3
stackstrace begin:
./test3(_Z16print_stacktracev+0x26) [0x4008e5]
./test3(_Z4fun1v+0x13) [0x4008a7]
./test3(_Z4fun2v+0x9) [0x4008b2]
./test3(_Z4fun3v+0x9) [0x4008bd]
./test3(main+0x9) [0x40088d]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xff) [0x7fa9558c1eff]
./test3() [0x4007c9]
wuzesheng@ubuntu:~/work/test$ addr2line -a 0x4008a7 -e test3 -f -C
0x00000000004008a7
fun1()
/home/wuzesheng/work/test/test.cc:20

      看上面的结果,我们不仅得到了调用栈,而且可以得到每个函数的名字,以及被调用的位置,大功告成。在这里需要说明一下的是,第3步比第2步多了一个-g选项,-g选项的主要作用是生成调试信息,位置信息就属于调试信息的范畴,经常用gdb的朋友相信不会对这个选项感到陌生。

おすすめ

転載: blog.csdn.net/daocaokafei/article/details/114968198