用C或C ++打印调用堆栈

时间:2010-10-10 10:11:55

标签: c++ c linux callstack

每次调用某个函数时,有没有办法在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上工作。


背景

我有一个测试运行,根据一些不应影响此行为的命令行开关,其行为会有所不同。我的代码有一个伪随机数生成器,我假设它是基于这些开关被不同地调用的。我希望能够使用每组开关运行测试,并查看随机数生成器是否针对每个开关进行不同的调用。

13 个答案:

答案 0 :(得分:70)

对于仅支持linux的解决方案,您可以使用backtrace(3)只返回void *的数组(实际上每个都指向相应堆栈帧的返回地址)。要将这些翻译成有用的东西,有backtrace_symbols(3)

注意notes section in backtrace(3)

  

符号名称可能不可用   不使用特殊的链接器   选项。          对于使用GNU链接器的系统,有必要使用    - 动态链接器          选项。请注意,“静态”函数的名称未公开,   并且不会          在回溯中可用。

答案 1 :(得分:7)

特定于Linux的TLDR:

    仅在链接glibc时,-lunwind中的
  1. backtrace才能生成准确的堆栈跟踪(未记录的平台特定功能)。
  2. 要输出功能名称源文件行号,请使用#include <elfutils/libdwfl.h>(此库仅在其头文件中记录)。 backtrace_symbolsbacktrace_symbolsd_fd信息最少。

在现代Linux上,您可以使用函数backtrace获取堆栈跟踪地址。使backtrace在流行平台上产生更准确地址的未记录方法是与-lunwind(在Ubuntu 18.04上为libunwind-dev)链接(请参见下面的示例输出)。 backtrace使用函数_Unwind_Backtrace,默认情况下,该函数来自libgcc_s.so.1,该实现是最可移植的。链接-lunwind时,它提供了_Unwind_Backtrace的更准确版本,但是此库的可移植性较差(请参见libunwind/src中支持的体系结构)。

不幸的是,相伴的backtrace_symbolsdbacktrace_symbols_fd函数大约十年来一直无法将堆栈跟踪地址解析为具有源文件名和行号的函数名(请参见下面的示例输出)

但是,还有另一种方法可以将地址解析为符号,它会生成具有函数名称源文件行号。该方法是#include <elfutils/libdwfl.h>并与-ldw链接(在Ubuntu 18.04上为libdw-dev)。

有效的C ++示例(test.cc):

#include <stdexcept>
#include <iostream>
#include <cassert>
#include <cstdlib>
#include <string>

#include <boost/core/demangle.hpp>

#include <execinfo.h>
#include <elfutils/libdwfl.h>

struct DebugInfoSession {
    Dwfl_Callbacks callbacks = {};
    char* debuginfo_path = nullptr;
    Dwfl* dwfl = nullptr;

    DebugInfoSession() {
        callbacks.find_elf = dwfl_linux_proc_find_elf;
        callbacks.find_debuginfo = dwfl_standard_find_debuginfo;
        callbacks.debuginfo_path = &debuginfo_path;

        dwfl = dwfl_begin(&callbacks);
        assert(dwfl);

        int r;
        r = dwfl_linux_proc_report(dwfl, getpid());
        assert(!r);
        r = dwfl_report_end(dwfl, nullptr, nullptr);
        assert(!r);
        static_cast<void>(r);
    }

    ~DebugInfoSession() {
        dwfl_end(dwfl);
    }

    DebugInfoSession(DebugInfoSession const&) = delete;
    DebugInfoSession& operator=(DebugInfoSession const&) = delete;
};

struct DebugInfo {
    void* ip;
    std::string function;
    char const* file;
    int line;

    DebugInfo(DebugInfoSession const& dis, void* ip)
        : ip(ip)
        , file()
        , line(-1)
    {
        // Get function name.
        uintptr_t ip2 = reinterpret_cast<uintptr_t>(ip);
        Dwfl_Module* module = dwfl_addrmodule(dis.dwfl, ip2);
        char const* name = dwfl_module_addrname(module, ip2);
        function = name ? boost::core::demangle(name) : "<unknown>";

        // Get source filename and line number.
        if(Dwfl_Line* dwfl_line = dwfl_module_getsrc(module, ip2)) {
            Dwarf_Addr addr;
            file = dwfl_lineinfo(dwfl_line, &addr, &line, nullptr, nullptr, nullptr);
        }
    }
};

std::ostream& operator<<(std::ostream& s, DebugInfo const& di) {
    s << di.ip << ' ' << di.function;
    if(di.file)
        s << " at " << di.file << ':' << di.line;
    return s;
}

void terminate_with_stacktrace() {
    void* stack[512];
    int stack_size = ::backtrace(stack, sizeof stack / sizeof *stack);

    // Print the exception info, if any.
    if(auto ex = std::current_exception()) {
        try {
            std::rethrow_exception(ex);
        }
        catch(std::exception& e) {
            std::cerr << "Fatal exception " << boost::core::demangle(typeid(e).name()) << ": " << e.what() << ".\n";
        }
        catch(...) {
            std::cerr << "Fatal unknown exception.\n";
        }
    }

    DebugInfoSession dis;
    std::cerr << "Stacktrace of " << stack_size << " frames:\n";
    for(int i = 0; i < stack_size; ++i) {
        std::cerr << i << ": " << DebugInfo(dis, stack[i]) << '\n';
    }
    std::cerr.flush();

    std::_Exit(EXIT_FAILURE);
}

int main() {
    std::set_terminate(terminate_with_stacktrace);
    throw std::runtime_error("test exception");
}

在具有gcc-8.3的Ubuntu 18.04.4 LTS上编译:

g++ -o test.o -c -m{arch,tune}=native -std=gnu++17 -W{all,extra,error} -g -Og -fstack-protector-all test.cc
g++ -o test -g test.o -ldw -lunwind

输出:

Fatal exception std::runtime_error: test exception.
Stacktrace of 7 frames:
0: 0x55f3837c1a8c terminate_with_stacktrace() at /home/max/src/test/test.cc:76
1: 0x7fbc1c845ae5 <unknown>
2: 0x7fbc1c845b20 std::terminate()
3: 0x7fbc1c845d53 __cxa_throw
4: 0x55f3837c1a43 main at /home/max/src/test/test.cc:103
5: 0x7fbc1c3e3b96 __libc_start_main at ../csu/libc-start.c:310
6: 0x55f3837c17e9 _start

当没有链接-lunwind时,它会产生不太准确的堆栈跟踪:

0: 0x5591dd9d1a4d terminate_with_stacktrace() at /home/max/src/test/test.cc:76
1: 0x7f3c18ad6ae6 <unknown>
2: 0x7f3c18ad6b21 <unknown>
3: 0x7f3c18ad6d54 <unknown>
4: 0x5591dd9d1a04 main at /home/max/src/test/test.cc:103
5: 0x7f3c1845cb97 __libc_start_main at ../csu/libc-start.c:344
6: 0x5591dd9d17aa _start

为了进行比较,对于相同的堆栈跟踪,backtrace_symbols_fd的输出提供的信息最少:

/home/max/src/test/debug/gcc/test(+0x192f)[0x5601c5a2092f]
/usr/lib/x86_64-linux-gnu/libstdc++.so.6(+0x92ae5)[0x7f95184f5ae5]
/usr/lib/x86_64-linux-gnu/libstdc++.so.6(_ZSt9terminatev+0x10)[0x7f95184f5b20]
/usr/lib/x86_64-linux-gnu/libstdc++.so.6(__cxa_throw+0x43)[0x7f95184f5d53]
/home/max/src/test/debug/gcc/test(+0x1ae7)[0x5601c5a20ae7]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe6)[0x7f9518093b96]
/home/max/src/test/debug/gcc/test(+0x1849)[0x5601c5a20849]

在生产版本(以及C语言版本)中,您可能希望通过将boost::core::demanglestd::stringstd::cout替换为其底层调用来使此代码更加健壮。

您还可以覆盖__cxa_throw以在引发异常时捕获堆栈跟踪,并在捕获异常时打印堆栈跟踪。当它进入catch块时,堆栈已经解开,因此调用backtrace为时已晚,这就是为什么必须在由功能实现的throw上捕获堆栈的原因__cxa_throw。请注意,在多线程程序中,__cxa_throw可以被多个线程同时调用,因此,如果它将堆栈跟踪捕获到必须为thread_local的全局数组中。

答案 2 :(得分:5)

没有标准化的方法可以做到这一点。对于Windows,功能在DbgHelp

中提供

答案 3 :(得分:5)

  

每次调用某个函数时,有没有办法在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)

此处提供完整的源代码。

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

答案 4 :(得分:4)

旧线程的另一个答案。

当我需要这样做时,我通常只使用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或简单的等价物,但是thread seems to have an alternative

答案 5 :(得分:4)

增强堆栈跟踪

记录在: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

请记住,回溯通常是优化无法修复的。尾部呼叫优化是一个著名的例子:What Is Tail Call Optimization?

与输出类似,在下面的“ glibc backtrace”部分中进行了进一步的解释。

在Ubuntu 18.04,GCC 7.3.0,boost 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

所以线只差了一个,TODO为什么呢?但这可能仍然可用。

结论:回溯只能用-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 backtrace相比有什么优势吗?非常相似的输出,也需要修改build命令,但使用范围较广。

代码改编自: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 abi::__cxa_demangle和libunwind,都可以使用backtrace完成,请参见以下示例:https://eli.thegreenplace.net/2015/programmatic-access-to-the-call-stack-in-c/

Linux内核

How to print the current thread stack trace inside the Linux kernel?

另请参见

答案 6 :(得分:2)

当然接下来的问题是:这还够吗?

堆栈跟踪的主要缺点是为什么你要调用精确的函数,你没有其他东西,比如它的参数值,这对于调试非常有用。

如果您有权访问gcc和gdb,我建议使用assert检查特定条件,如果不满足则生成内存转储。当然这意味着该过程将停止,但您将获得完整的报告,而不仅仅是堆栈跟踪。

如果您希望采用不那么突兀的方式,可以随时使用日志记录。那里有非常高效的日志记录设施,例如Pantheios。这再一次可以让你更准确地了解正在发生的事情。

答案 7 :(得分:2)

您可以使用Poppy。它通常用于在崩溃期间收集堆栈跟踪,但它也可以为正在运行的程序输出它。

现在这里有好处:它可以输出堆栈上每个函数的实际参数值,甚至是局部变量,循环计数器等。

答案 8 :(得分:1)

您可以自己实现这些功能:

使用全局(字符串)堆栈,并在每个函数的开头将函数名称和其他值(例如参数)推送到此堆栈;在退出功能时再次弹出它。

编写一个函数,在调用堆栈内容时将其打印出来,并在要查看调用堆栈的函数中使用它。

这可能听起来很多,但非常有用。

答案 9 :(得分:1)

我知道这个帖子已经过时了,但我认为它对其他人有用。如果您使用的是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

答案 10 :(得分:1)

您可以使用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

答案 11 :(得分:0)

您可以使用GNU分析器。它也显示了调用图!命令是gprof,您需要使用一些选项编译代码。

答案 12 :(得分:-3)

  

每次调用某个函数时,有没有办法在C或C ++中正在运行的进程中转储调用堆栈?

不存在,尽管可能存在与平台相关的解决方案。