ChatGPT解决这个技术问题 Extra ChatGPT

在 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 上工作。

背景

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

@Armen,你熟悉这些吗?
@Nathan:如果您的调试器是 gdb,它可以handle that case。我不能告诉你其他人,但我认为 gdb 并不是唯一一个拥有这个功能的人。 旁白:我只是看了我之前的评论。 ::gag:: s/easier/either/这到底是怎么回事?
@dmckee:其实应该是s/either/easier。我需要对 gdb 做的是编写一个脚本来中断该函数并打印出堆栈跟踪,然后继续。现在我想了想,也许是我学习 gdb 脚本的时候了。
呸!要去睡觉了。现在真的快了...

I
Idan K

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

注意notes section in backtrace(3)

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


FWIW,此功能也存在于 Mac OS X 上:developer.apple.com/library/mac/#documentation/Darwin/Reference/…
Windows 有 CaptureStackBackTrace
遗憾的是,在带有 glibc 的 Linux 上,backtrace_symbols 函数不提供函数名、源文件名和行号。
除了使用 -rdynamic 之外,还要检查您的构建系统是否没有添加 -fvisibility=hidden 选项! (因为它会完全放弃 -rdynamic 的效果)
苹果又换了链接! developer.apple.com/library/archive/documentation/System/…(我知道 URL 显示“iPhoneOS”,但手册页本身显示“此文档是 Mac OS X 手册页”)。
C
Ciro Santilli Путлер Капут 六四事

提升堆栈跟踪

记录于: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 只是标题,因此很可能不需要修改您的构建系统

boost_stacktrace.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) {
    (void)f;
    my_func_2();
}

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

int main(int argc, char **argv) {
    long long unsigned int n;
    if (argc > 1) {
        n = strtoul(argv[1], NULL, 0);
    } else {
        n = 1;
    }
    for (long long unsigned int i = 0; i < n; ++i) {
        my_func_1(1);   // line 28
        my_func_1(2.0); // line 29
    }
}

不幸的是,它似乎是最近添加的,并且包 libboost-stacktrace-dev 在 Ubuntu 16.04 中不存在,只有 18.04:

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

我们必须在最后添加 -ldl 否则编译失败。

输出:

 0# boost::stacktrace::basic_stacktrace<std::allocator<boost::stacktrace::frame> >::basic_stacktrace() at /usr/include/boost/stacktrace/stacktrace.hpp:129
 1# my_func_1(int) at /home/ciro/test/boost_stacktrace.cpp:18
 2# main at /home/ciro/test/boost_stacktrace.cpp:29 (discriminator 2)
 3# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 4# _start in ./boost_stacktrace.out

 0# boost::stacktrace::basic_stacktrace<std::allocator<boost::stacktrace::frame> >::basic_stacktrace() at /usr/include/boost/stacktrace/stacktrace.hpp:129
 1# my_func_1(double) at /home/ciro/test/boost_stacktrace.cpp:13
 2# main at /home/ciro/test/boost_stacktrace.cpp:27 (discriminator 2)
 3# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 4# _start in ./boost_stacktrace.out

输出并在下面的“glibc 回溯”部分进一步解释,这是类似的。

请注意 my_func_1(int)my_func_1(float)which are mangled due to function overload 是如何为我们很好地分解的。

请注意,第一个 int 调用关闭了一行(28 而不是 27,第二个调用关闭了两行(27 而不是 29)。这是 suggested in the comments,这是因为正在考虑以下指令地址,这使得 27 变为 28,并且 29 跳出循环并变为 27。

然后我们观察到使用 -O3,输出被完全破坏:

 0# boost::stacktrace::basic_stacktrace<std::allocator<boost::stacktrace::frame> >::size() const at /usr/include/boost/stacktrace/stacktrace.hpp:215
 1# my_func_1(double) at /home/ciro/test/boost_stacktrace.cpp:12
 2# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 3# _start in ./boost_stacktrace.out

 0# boost::stacktrace::basic_stacktrace<std::allocator<boost::stacktrace::frame> >::size() const at /usr/include/boost/stacktrace/stacktrace.hpp:215
 1# main at /home/ciro/test/boost_stacktrace.cpp:31
 2# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 3# _start in ./boost_stacktrace.out

回溯通常被优化不可挽回地破坏。尾调用优化就是一个值得注意的例子:What is tail call optimization?

-O3 上运行基准测试:

time  ./boost_stacktrace.out 1000 >/dev/null

输出:

real    0m43.573s
user    0m30.799s
sys     0m13.665s

因此,正如预期的那样,我们看到此方法可能对外部调用 addr2line 非常慢,并且只有在进行有限数量的调用时才可行。

每次回溯打印似乎需要数百毫秒,因此请注意,如果回溯经常发生,程序性能将受到严重影响。

在 Ubuntu 19.10、GCC 9.2.1、boost 1.67.0 上测试。

glibc backtrace

记录于:https://www.gnu.org/software/libc/manual/html_node/Backtraces.html

主程序

#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 完美显示。通过优化,原始回溯在编译后的代码中得到了根本性的修改。

但是,我找不到一种简单的方法来自动对 C++ 符号进行解码,这里有一些技巧:

https://panthema.net/2008/0901-stacktrace-demangled/

https://gist.github.com/fmela/591333/c64f4eb86037bb237862a8283df70cdfc25f01d3

在 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 上测试。

glibc backtrace 与 C++ 解构技巧 1:-export-dynamic + dladdr

改编自:https://gist.github.com/fmela/591333/c64f4eb86037bb237862a8283df70cdfc25f01d3

这是一个“hack”,因为它需要使用 -export-dynamic 更改 ELF。

glibc_ldl.cpp

#include <dlfcn.h>     // for dladdr
#include <cxxabi.h>    // for __cxa_demangle

#include <cstdio>
#include <string>
#include <sstream>
#include <iostream>

// This function produces a stack backtrace with demangled function & method names.
std::string backtrace(int skip = 1)
{
    void *callstack[128];
    const int nMaxFrames = sizeof(callstack) / sizeof(callstack[0]);
    char buf[1024];
    int nFrames = backtrace(callstack, nMaxFrames);
    char **symbols = backtrace_symbols(callstack, nFrames);

    std::ostringstream trace_buf;
    for (int i = skip; i < nFrames; i++) {
        Dl_info info;
        if (dladdr(callstack[i], &info)) {
            char *demangled = NULL;
            int status;
            demangled = abi::__cxa_demangle(info.dli_sname, NULL, 0, &status);
            std::snprintf(
                buf,
                sizeof(buf),
                "%-3d %*p %s + %zd\n",
                i,
                (int)(2 + sizeof(void*) * 2),
                callstack[i],
                status == 0 ? demangled : info.dli_sname,
                (char *)callstack[i] - (char *)info.dli_saddr
            );
            free(demangled);
        } else {
            std::snprintf(buf, sizeof(buf), "%-3d %*p\n",
                i, (int)(2 + sizeof(void*) * 2), callstack[i]);
        }
        trace_buf << buf;
        std::snprintf(buf, sizeof(buf), "%s\n", symbols[i]);
        trace_buf << buf;
    }
    free(symbols);
    if (nFrames == nMaxFrames)
        trace_buf << "[truncated]\n";
    return trace_buf.str();
}

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

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

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

int main() {
    my_func_1(1);
    my_func_1(2.0);
}

编译并运行:

g++ -fno-pie -ggdb3 -O0 -no-pie -o glibc_ldl.out -std=c++11 -Wall -Wextra \
  -pedantic-errors -fpic glibc_ldl.cpp -export-dynamic -ldl
./glibc_ldl.out 

输出:

1             0x40130a my_func_2() + 41
./glibc_ldl.out(_Z9my_func_2v+0x29) [0x40130a]
2             0x40139e my_func_1(int) + 16
./glibc_ldl.out(_Z9my_func_1i+0x10) [0x40139e]
3             0x4013b3 main + 18
./glibc_ldl.out(main+0x12) [0x4013b3]
4       0x7f7594552b97 __libc_start_main + 231
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7) [0x7f7594552b97]
5             0x400f3a _start + 42
./glibc_ldl.out(_start+0x2a) [0x400f3a]

1             0x40130a my_func_2() + 41
./glibc_ldl.out(_Z9my_func_2v+0x29) [0x40130a]
2             0x40138b my_func_1(double) + 18
./glibc_ldl.out(_Z9my_func_1d+0x12) [0x40138b]
3             0x4013c8 main + 39
./glibc_ldl.out(main+0x27) [0x4013c8]
4       0x7f7594552b97 __libc_start_main + 231
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7) [0x7f7594552b97]
5             0x400f3a _start + 42
./glibc_ldl.out(_start+0x2a) [0x400f3a]

在 Ubuntu 18.04 上测试。

glibc backtrace 与 C++ 解构技巧 2:解析回溯输出

显示在:https://panthema.net/2008/0901-stacktrace-demangled/

这是一个 hack,因为它需要解析。

TODO 让它编译并在此处显示。

自由风

TODO 这比 glibc 回溯有什么优势吗?非常相似的输出,也需要修改构建命令,但不是 glibc 的一部分,因此需要额外的包安装。

代码改编自:https://eli.thegreenplace.net/2015/programmatic-access-to-the-call-stack-in-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

linux 上不再定义类型“stack_t”了吗?

Glibc - ucontext.h 中的错误,但仅限于 -std=c11

跑:

./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++ 名称的 libunwind

代码改编自:https://eli.thegreenplace.net/2015/programmatic-access-to-the-call-stack-in-c/

展开.cpp

#define UNW_LOCAL_ONLY
#include <cxxabi.h>
#include <libunwind.h>
#include <cstdio>
#include <cstdlib>
#include <iostream>

void backtrace() {
  unw_cursor_t cursor;
  unw_context_t context;

  // Initialize cursor to current frame for local unwinding.
  unw_getcontext(&context);
  unw_init_local(&cursor, &context);

  // Unwind frames one by one, going up the frame stack.
  while (unw_step(&cursor) > 0) {
    unw_word_t offset, pc;
    unw_get_reg(&cursor, UNW_REG_IP, &pc);
    if (pc == 0) {
      break;
    }
    std::printf("0x%lx:", pc);

    char sym[256];
    if (unw_get_proc_name(&cursor, sym, sizeof(sym), &offset) == 0) {
      char* nameptr = sym;
      int status;
      char* demangled = abi::__cxa_demangle(sym, nullptr, nullptr, &status);
      if (status == 0) {
        nameptr = demangled;
      }
      std::printf(" (%s+0x%lx)\n", nameptr, offset);
      std::free(demangled);
    } else {
      std::printf(" -- error: unable to obtain symbol name for this frame\n");
    }
  }
}

void my_func_2(void) {
    backtrace();
    std::cout << std::endl; // line 43
}

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

void my_func_1(int i) {
    (void)i;
    my_func_2();
}  // line 54

int main() {
    my_func_1(1);
    my_func_1(2.0);
}

编译并运行:

sudo apt-get install libunwind-dev
g++ -fno-pie -ggdb3 -O0 -no-pie -o unwind.out -std=c++11 \
  -Wall -Wextra -pedantic-errors unwind.cpp -lunwind -pthread
./unwind.out

输出:

0x400c80: (my_func_2()+0x9)
0x400cb7: (my_func_1(int)+0x10)
0x400ccc: (main+0x12)
0x7f4c68926b97: (__libc_start_main+0xe7)
0x400a3a: (_start+0x2a)

0x400c80: (my_func_2()+0x9)
0x400ca4: (my_func_1(double)+0x12)
0x400ce1: (main+0x27)
0x7f4c68926b97: (__libc_start_main+0xe7)
0x400a3a: (_start+0x2a)

然后我们可以找到 my_func_2my_func_1(int) 的行:

addr2line -e unwind.out 0x400c80 0x400cb7

这使:

/home/ciro/test/unwind.cpp:43
/home/ciro/test/unwind.cpp:54

待办事项:为什么线条会偏离一条?

在 Ubuntu 18.04、GCC 7.4.0、libunwind 1.2.1 上测试。

GDB 自动化

我们也可以使用 GDB 执行此操作而无需重新编译,方法是:How to do an specific action when a certain breakpoint is hit in GDB?

虽然如果您要打印很多回溯,这可能会比其他选项快,但也许我们可以使用 compile code 达到本机速度,但我现在懒得测试它:How to call assembly in gdb?

主文件

void my_func_2(void) {}

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

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

int main() {
    my_func_1(1);
    my_func_1(2.0);
}

主数据库

start
break my_func_2
commands
  silent
  backtrace
  printf "\n"
  continue
end
continue

编译并运行:

g++ -ggdb3 -o main.out main.cpp
gdb -nh -batch -x main.gdb main.out

输出:

Temporary breakpoint 1 at 0x1158: file main.cpp, line 12.

Temporary breakpoint 1, main () at main.cpp:12
12          my_func_1(1);
Breakpoint 2 at 0x555555555129: file main.cpp, line 1.
#0  my_func_2 () at main.cpp:1
#1  0x0000555555555151 in my_func_1 (i=1) at main.cpp:8
#2  0x0000555555555162 in main () at main.cpp:12

#0  my_func_2 () at main.cpp:1
#1  0x000055555555513e in my_func_1 (f=2) at main.cpp:4
#2  0x000055555555516f in main () at main.cpp:13

[Inferior 1 (process 14193) exited normally]

待办事项 我想只使用命令行中的 -ex 来执行此操作,而不必创建 main.gdb,但我无法让 commands 在那里工作。

在 Ubuntu 19.04、GDB 8.2 中测试。

Linux内核

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

libdwfl

这最初是在:https://stackoverflow.com/a/60713161/895245 中提到的,它可能是最好的方法,但我必须进行更多基准测试,但请投票支持该答案。

TODO:我试图将那个有效的答案中的代码最小化为一个函数,但它是段错误的,如果有人能找到原因,请告诉我。

dwfl.cpp

#include <cassert>
#include <iostream>
#include <memory>
#include <sstream>
#include <string>

#include <cxxabi.h> // __cxa_demangle
#include <elfutils/libdwfl.h> // Dwfl*
#include <execinfo.h> // backtrace
#include <unistd.h> // getpid

// https://stackoverflow.com/questions/281818/unmangling-the-result-of-stdtype-infoname
std::string demangle(const char* name) {
    int status = -4;
    std::unique_ptr<char, void(*)(void*)> res {
        abi::__cxa_demangle(name, NULL, NULL, &status),
        std::free
    };
    return (status==0) ? res.get() : name ;
}

std::string debug_info(Dwfl* dwfl, void* ip) {
    std::string function;
    int line = -1;
    char const* file;
    uintptr_t ip2 = reinterpret_cast<uintptr_t>(ip);
    Dwfl_Module* module = dwfl_addrmodule(dwfl, ip2);
    char const* name = dwfl_module_addrname(module, ip2);
    function = name ? demangle(name) : "<unknown>";
    if (Dwfl_Line* dwfl_line = dwfl_module_getsrc(module, ip2)) {
        Dwarf_Addr addr;
        file = dwfl_lineinfo(dwfl_line, &addr, &line, nullptr, nullptr, nullptr);
    }
    std::stringstream ss;
    ss << ip << ' ' << function;
    if (file)
        ss << " at " << file << ':' << line;
    ss << std::endl;
    return ss.str();
}

std::string stacktrace() {
    // Initialize Dwfl.
    Dwfl* dwfl = nullptr;
    {
        Dwfl_Callbacks callbacks = {};
        char* debuginfo_path = nullptr;
        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);
    }

    // Loop over stack frames.
    std::stringstream ss;
    {
        void* stack[512];
        int stack_size = ::backtrace(stack, sizeof stack / sizeof *stack);
        for (int i = 0; i < stack_size; ++i) {
            ss << i << ": ";

            // Works.
            ss << debug_info(dwfl, stack[i]);

#if 0
            // TODO intended to do the same as above, but segfaults,
            // so possibly UB In above function that does not blow up by chance?
            void *ip = stack[i];
            std::string function;
            int line = -1;
            char const* file;
            uintptr_t ip2 = reinterpret_cast<uintptr_t>(ip);
            Dwfl_Module* module = dwfl_addrmodule(dwfl, ip2);
            char const* name = dwfl_module_addrname(module, ip2);
            function = name ? demangle(name) : "<unknown>";
            // TODO if I comment out this line it does not blow up anymore.
            if (Dwfl_Line* dwfl_line = dwfl_module_getsrc(module, ip2)) {
              Dwarf_Addr addr;
              file = dwfl_lineinfo(dwfl_line, &addr, &line, nullptr, nullptr, nullptr);
            }
            ss << ip << ' ' << function;
            if (file)
                ss << " at " << file << ':' << line;
            ss << std::endl;
#endif
        }
    }
    dwfl_end(dwfl);
    return ss.str();
}

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

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

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

int main(int argc, char **argv) {
    long long unsigned int n;
    if (argc > 1) {
        n = strtoul(argv[1], NULL, 0);
    } else {
        n = 1;
    }
    for (long long unsigned int i = 0; i < n; ++i) {
        my_func_1(1);   // line 122
        my_func_1(2.0); // line 123
    }
}

编译并运行:

sudo apt install libdw-dev libunwind-dev
g++ -fno-pie -ggdb3 -O0 -no-pie -o dwfl.out -std=c++11 -Wall -Wextra -pedantic-errors dwfl.cpp -ldw -lunwind
./dwfl.out

我们还需要 libunwind,因为这会使结果更正确。如果没有它,它会运行,但你会发现有些行有点错误。

输出:

0: 0x402b72 stacktrace[abi:cxx11]() at /home/ciro/test/dwfl.cpp:65
1: 0x402cda my_func_2() at /home/ciro/test/dwfl.cpp:100
2: 0x402d76 my_func_1(int) at /home/ciro/test/dwfl.cpp:111
3: 0x402dd1 main at /home/ciro/test/dwfl.cpp:122
4: 0x7ff227ea0d8f __libc_start_call_main at ../sysdeps/nptl/libc_start_call_main.h:58
5: 0x7ff227ea0e3f __libc_start_main@@GLIBC_2.34 at ../csu/libc-start.c:392
6: 0x402534 _start at ../csu/libc-start.c:-1

0: 0x402b72 stacktrace[abi:cxx11]() at /home/ciro/test/dwfl.cpp:65
1: 0x402cda my_func_2() at /home/ciro/test/dwfl.cpp:100
2: 0x402d5f my_func_1(double) at /home/ciro/test/dwfl.cpp:106
3: 0x402de2 main at /home/ciro/test/dwfl.cpp:123
4: 0x7ff227ea0d8f __libc_start_call_main at ../sysdeps/nptl/libc_start_call_main.h:58
5: 0x7ff227ea0e3f __libc_start_main@@GLIBC_2.34 at ../csu/libc-start.c:392
6: 0x402534 _start at ../csu/libc-start.c:-1

基准运行:

g++ -fno-pie -ggdb3 -O3 -no-pie -o dwfl.out -std=c++11 -Wall -Wextra -pedantic-errors dwfl.cpp -ldw
time ./dwfl.out 1000 >/dev/null

输出:

real    0m3.751s
user    0m2.822s
sys     0m0.928s

所以我们看到这种方法比 Boost 的堆栈跟踪快 10 倍,因此可能适用于更多用例。

在 Ubuntu 22.04 amd64、libdw-dev 0.186、libunwind 1.3.2 中测试。

libbacktrace

https://github.com/ianlancetaylor/libbacktrace

考虑到 harcore 库作者,值得尝试一下,也许是 The One。待办事项检查一下。

可链接到 C/C++ 程序以生成符号回溯的 AC 库截至 2020 年 10 月,libbacktrace 支持带有 DWARF 调试信息的 ELF、PE/COFF、Mach-O 和 XCOFF 可执行文件。换句话说,它支持 GNU/Linux、*BSD、macOS、Windows 和 AIX。编写该库是为了方便地添加对其他目标文件和调试格式的支持。该库依赖于 https://itanium-cxx-abi.github.io/cxx-abi/abi-eh.html 中定义的 C++ 展开 API。此 API 由 GCC 和 clang 提供。

也可以看看

如何在 C 中获取堆栈跟踪?

如何让 backtrace()/backtrace_symbols() 打印函数名?

是否有一种可移植/符合标准的方法来获取堆栈跟踪中的文件名和行号?

从程序内部调用 gdb 以打印其堆栈跟踪的最佳方法?

失败时自动堆栈跟踪:C++ 异常:C++ 显示异常通用堆栈跟踪:如何在我的程序崩溃时自动生成堆栈跟踪

关于 C++ 异常:C++ 显示异常时的堆栈跟踪

通用:当我的程序崩溃时如何自动生成堆栈跟踪


所有“TODO: lines off by one”都是因为行号是从下一个表达式的开头获取的。
使用 libdwfl 方法,我得到了很好的模块名称,但没有源文件/行号。 Intead,dwfl_module_getsrc 总是返回 21“地址超出范围”。您对可能出现的问题或如何解决此问题有任何建议吗? (无论是否有指向任何 exe 或 .o 文件所在的每个目录的 debuginfo_path 都会发生这种情况。)
我还应该说没有获取文件/行#s 的问题:我正在使用带有 --unwindlib=libgccclang,而不是似乎不起作用的 -lunwind
@davidbak 我刚刚重新测试了 libdwfl 示例,它在 Ubuntu 22.04 上运行。您为什么要尝试将 -lunwind 传递给 dwfl 示例?
@davidbak 啊,好吧,我没看到。我没有注意到,但没有 -libunwind 时,有些行实际上有点错误。更新以使用它。那么clang绝对是您案件中最大的怀疑。
N
NullPointerException

每次调用某个函数时,有没有办法将调用堆栈转储到 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


M
Maxim Egorushkin

特定于 Linux 的 TLDR:

glibc 中的回溯仅在链接 -lunwind 时产生准确的堆栈跟踪(未记录的平台特定功能)。要输出函数名称、源文件和行号,请使用 #include (此库仅记录在其头文件中)。 backtrace_symbols 和 backtrace_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 的全局数组中。

您还可以制作堆栈跟踪打印函数 async-signal safe,以便您可以直接从 SIGSEGVSIGBUS 信号处理程序调用它(它们应该使用自己的堆栈以实现稳健性)。使用 libdwfl 从信号处理程序获取 函数名源文件行号 可能会失败,因为它不是异步信号安全的,或者如果该进程的地址空间已被严重损坏,但实际上它有 99% 的时间成功(我没有看到它失败)。

总而言之,用于自动堆栈跟踪输出的完整生产就绪库应该:

在 throw 到特定于线程的存储时捕获堆栈跟踪。在未处理的异常上自动打印堆栈跟踪。以异步信号安全的方式打印堆栈跟踪。提供一个强大的信号处理函数,它使用自己的堆栈,以异步信号安全的方式打印堆栈跟踪。用户可以将此函数安装为 SIGSEGV、SIGBUS、SIGFPE 等的信号处理程序。信号处理程序还可以从 ucontext_t 信号函数参数(可能不包括向量寄存器)打印故障点处所有 CPU 寄存器的值),a-la Linux 内核 oops 日志消息。


这个 -lunwind 问题是在发布此帖子时发现的,我之前直接使用 libunwind 来获取堆栈跟踪并准备发布它,但是当 -lunwind 被链接时,backtrace 为我完成了它。
GCC 的 _Unwind_backtrace 的可移植性如何不如 libunwind 的?
@SSAnne 我说 gcc 版本是 most 可移植的,因为它用于 catch
有什么具体原因吗?是用asm手写的吗?
@SSAnne 可能是因为库 David Mosberger 的原始作者最初专注于 IA-64,但后来库得到了更多的关注 nongnu.org/libunwind/people.htmlgcc 不公开 API,对吗?
P
Paul Floyd

旧线程的另一个答案。

当我需要这样做时,我通常只使用 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

如果您使用 C,那么您将需要使用 C 字符串函数。

#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

void f()
{
    pid_t myPid = getpid();
    /*
      length of command 7 for 'pstack ', 7 for the PID, 1 for nul
    */
    char pstackCommand[7+7+1];
    sprintf(pstackCommand, "pstack %d", (int)myPid);
    system(pstackCommand);
}

我使用 7 作为 PID 中的最大位数,基于 this post


好点,因为该主题确实要求使用 C。不,它需要调整,因为 std::string 仅是 C++。我将用 C 版本更新我的答案。
l
luizfls

在 C++23 中,会有 <stacktrace>,然后你可以这样做:

#include <stacktrace>

/* ... */

std::cout << std::stacktrace::current();

更多详情:
  • https://en.cppreference.com/w/cpp/header/stacktrace
  • https://en.cppreference.com/w/cpp/utility/basic_stacktrace/operator_ltlt


P
Paul Michalik

没有标准化的方法可以做到这一点。对于 Windows,DbgHelp 库中提供了该功能


有什么例子吗?我从 msdn 得到的只是它的唯一功能是获取符号,与调用堆栈无关
B
Barkles

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


我在 Win10 上收到错误 cannot locate SymEnumSymbolsExW at C:\Windows\SYSTEM32\dbgeng.dll
s
slashmais

您可以自己实现该功能:

使用全局(字符串)堆栈,并在每个函数开始时将函数名称和其他值(例如参数)压入此堆栈;在函数退出时再次弹出它。

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

这听起来可能需要做很多工作,但非常有用。


我不会那样做。相反,我会创建一个使用底层平台特定 API 的包装器(见下文)。工作量可能相同,但投资回报应该更快。
@paul:当 OP 明确指定 linux 时,您的答案指的是 windows ......但对于出现在这里的 windows-guys 可能很有用。
对,我忽略了..嗯,这是问题的最后一句话,所以也许发帖人应该修改他的请求,在更显眼的地方提及他/她的目标平台。
这将是一个好主意,除了我的代码库包含几十个文件,其中包含几百个(如果不是几千个)文件,所以这是不可行的。
如果您在每个函数声明之后添加一个 sed/perl 脚本,则可能不会。call_registror MY_SUPERSECRETNAME(__FUNCTION__); 在其构造函数中推送参数并在其析构函数中弹出 FUNCTION 始终代表当前函数的名称。
F
François

我知道这个线程很旧,但我认为它对其他人有用。如果你使用 gcc,你可以使用它的工具特性(-finstrument-functions 选项)来记录任何函数调用(进入和退出)。查看此内容以获取更多信息:http://hacktalks.blogspot.fr/2013/08/gcc-instrument-functions.html

例如,您可以将每个调用推送和弹出到堆栈中,当您想要打印它时,您只需查看堆栈中的内容。

我已经测试过了,它工作得很好,非常方便

更新:您还可以在 GCC 文档中找到有关 Instrumentation 选项的 -finstrument-functions 编译选项的信息:https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html


您还应该链接到 GCC 文档,以防文章失败。
谢谢你,你是对的。因此,我在我的帖子中添加了一个更新,其中包含指向 gcc 文档的链接
M
Matthieu M.

当然,下一个问题是:这就够了吗?

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

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

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


当然这可能还不够,但如果我可以看到该函数是在一种配置中调用的,而不是在另一种配置中调用的,那么这是一个很好的起点。
O
Orlin Georgiev

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

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


T
Taryn

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


s
sbi

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

不,没有,尽管可能存在依赖于平台的解决方案。