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

15 浏览
0 Comments

打印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库中是否存在这样的东西?我正在Linux上使用GCC。

背景

我有一个测试运行,基于一些命令行开关表现不同,但不应影响该行为。我的代码有一个伪随机数生成器,我假设基于这些开关调用方式不同。我想能够在每组开关下运行测试,并查看每个开关是否会不同地调用伪随机数生成器。

admin 更改状态以发布 2023年5月23日
0
0 Comments

如果你只想用 Linux 的解决方案,你可以使用 backtrace(3),它会简单地返回一个 void * 数组(实际上,每个数组元素都指向相应栈帧的返回地址)。为了将这些转换为有用的东西,可以使用 backtrace_symbols(3)

注意 backtrace(3) 中的注释部分:

没有使用特殊链接器选项可能导致无法获取符号名称。对于使用 GNU 链接器的系统,必须使用 -rdynamic 链接器选项。请注意,“静态”函数的名称是不公开的,并且不会在回溯中提供。

0
0 Comments

C/C++回溯方法调查

在本答案中,我将尝试为一堆解决方案运行单个基准测试,看看哪个运行速度更快,同时考虑其他因素,如功能和可移植性。

工具 时间/调用 行号 函数名 C ++重述 重新编译 信号安全 作为字符串 C
C++23 GCC 12.1 7微秒 y y y y n y n
Boost 1.74stacktrace() 5微秒 y y y y n y n
Boost 1.74 stacktrace::safe_dump_to y n n
glibc backtrace_symbols_fd 25微秒 n -rdynamic 黑客 y y n y
glibc backtrace_symbols 21微秒 n -rdynamic 黑客 y n y y
GDB脚本 600微秒 y y y n y n y
GDB代码注入 n n y
libunwind y
libdwfl 4毫秒 n y
libbacktrace y

空格单元格表示“TODO”,而不是“no”。

  • us:微秒

  • 行号:显示实际行号,而不仅仅是函数名+内存地址。

    通常可以使用addr2line手动重新获取地址的行号。但这是令人烦恼的。

  • 重新编译:需要重新编译程序以获取跟踪。不重新编译更好!

  • 信号安全:对于“在段错误情况下获取堆栈跟踪”的重要用例至关重要:在我的程序崩溃时如何自动生成堆栈跟踪

  • 作为字符串:您在程序中以字符串格式获得堆栈跟踪,而不是仅仅打印到STDOUT等。通常意味着不是信号安全的,因为我们不知道堆栈跟踪字符串大小的大小,因此需要malloc,这不是异步信号安全的。

  • C:它能够在一个纯C项目中工作(是的,仍然有可怜的灵魂),还是需要C++?

测试设置

所有基准测试将运行以下内容

main.cpp

#include  // strtoul
#include 
void my_func_2(void) {
    print_stacktrace(); // line 6
}
void my_func_1(double f) {
    (void)f;
    my_func_2();
}
void my_func_1(int i) {
    (void)i;
    my_func_2(); // line 16
}
int main(int argc, char **argv) {
    long long unsigned int n;
    if (argc > 1) {
        n = std::strtoul(argv[1], NULL, 0);
    } else {
        n = 1;
    }
    for (long long unsigned int i = 0; i < n; ++i) {
        my_func_1(1); // line 27
    }
}

这个输入的设计是为了测试C++名称重整,因为my_func_1(int)my_func_1(float)必须作为实现C++函数重载的一种方式而重整名称。

我们使用不同的-I includes来区分基准测试, 它们指向不同的print_stacktrace()的实现。

每个基准测试都用以下格式的命令进行:

time ./stacktrace.out 100000 &>/dev/null

为了产生每个基准测试的总运行时间为1s左右,会根据每个实现调整迭代次数。

下面的所有测试都使用-O0,除非有特别注明。某些优化可能会彻底破坏堆栈跟踪。尾递归优化就是其中的一个明显例子:什么是尾递归优化?我们无能为力。

C++23

此方法之前在https://stackoverflow.com/a/69384663/895245中提到,请考虑给那个答案点赞。

这是最好的解决方法......它是可移植的,快速的,显示行号和反编译C++符号。一旦它变得更加普遍可用,它将取代任何其他选择,除非是需要不重新编译的个别情况下的GDB。

cpp20_stacktrace/mystacktrace.h

#include 
#include 
void print_stacktrace() {
    std::cout << std::stacktrace::current();
}

来自Ubuntu 22.04的GCC 12.1.0没有编译支持,因此我暂时按照如何编辑和重新构建GCC libstdc++ C++标准库源码?从源代码构建它,并设置--enable-libstdcxx-backtrace=yes,它工作了!

使用以下命令进行编译:

g++ -O0 -ggdb3 -Wall -Wextra -pedantic -std=c++23 -o cpp20_stacktrace.out main.cpp -lstdc++_libbacktrace

样本输出:

   0# print_stacktrace() at cpp20_stacktrace/mystacktrace.h:5
   1# my_func_2() at /home/ciro/main.cpp:6
   2# my_func_1(int) at /home/ciro/main.cpp:16
   3#      at /home/ciro/main.cpp:27
   4#      at :0
   5#      at :0
   6#      at :0
   7#

如果我们尝试使用Ubuntu 22.04的GCC 12.1.0:

sudo apt install g++-12
g++-12 -ggdb3 -O2 -std=c++23 -Wall -Wextra -pedantic -o stacktrace.out stacktrace.cpp -lstdc++_libbacktrace

它将失败并显示:

stacktrace.cpp: In function ‘void my_func_2()’:
stacktrace.cpp:6:23: error: ‘std::stacktrace’ has not been declared
    6 |     std::cout << std::stacktrace::current();
      |                       ^~~~~~~~~~

通过使用以下命令检查构建选项:

g++-12 -v

我们并没有看到:

--enable-libstdcxx-backtrace=yes

所以它没有被编译进去。引用资料:

它不会因为头文件的问题而失败:

/usr/include/c++/12

具有功能检查的特性:

#if __cplusplus > 202002L && _GLIBCXX_HAVE_STACKTRACE

Boost stacktrace

该库在Ubuntu 22.04周围发生了很多变化,因此请确保您的版本与之匹配:Boost stack-trace not showing function names and line numbers

该库几乎被更便携的C++23实现所取代,但对于那些还未达到该标准版本但已经获得“Boost透明度”的人来说,它仍然是一个非常好的选择。

文件位置:https://www.boost.org/doc/libs/1_66_0/doc/html/stacktrace/getting_started.html#stacktrace.getting_started.how_to_print_current_call_stack

在Ubuntu 22.04、boost 1.74.0上测试,您应该执行以下操作:

boost_stacktrace/mystacktrace.h

#include 
#define BOOST_STACKTRACE_LINK
#include 
void print_stacktrace(void) {
    std::cout << boost::stacktrace::stacktrace();
}

在Ubuntu 19.10 boost 1.67.0上,为了显示行号,我们必须执行以下操作:

#include 
#define BOOST_STACKTRACE_USE_ADDR2LINE
#include 
void print_stacktrace(void) {
    std::cout << boost::stacktrace::stacktrace();
}

这将调用addr2line可执行文件,比较新的Boost版本慢1000倍。

在Ubuntu 16.04上,libboost-stacktrace-dev软件包根本不存在。

这部分的其余部分仅考虑Ubuntu 22.04,boost 1.74的行为。

编译:

sudo apt-get install libboost-stacktrace-dev
g++ -O0 -ggdb3 -Wall -Wextra -pedantic -std=c++11 -o boost_stacktrace.out main.cpp -lboost_stacktrace_backtrace

示例输出:

 0# print_stacktrace() at boost_stacktrace/mystacktrace.h:7
 1# my_func_2() at /home/ciro/main.cpp:7
 2# my_func_1(int) at /home/ciro/main.cpp:17
 3# main at /home/ciro/main.cpp:26
 4# __libc_start_call_main at ../sysdeps/nptl/libc_start_call_main.h:58
 5# __libc_start_main at ../csu/libc-start.c:379
 6# _start in ./boost_stacktrace.out

请注意,行数有一行偏差。评论中建议这是因为考虑了以下指令地址。

Boost stacktrace仅包含头文件

BOOST_STACKTRACE_LINK的作用是在链接时需要-lboost_stacktrace_backtrace,所以我们想象没有这个将会正常工作。这对于没有“Boost透明度”的开发人员来说是一个很好的选择,只需添加一个调试。

TODO,不幸的是,它对我来说没有很好的效果:

#include 
#include 
void print_stacktrace(void) {
    std::cout << boost::stacktrace::stacktrace();
}

然后:

g++ -O0 -ggdb3 -Wall -Wextra -pedantic -std=c++11 -o boost_stacktrace_header_only.out main.cpp

包含过度短的输出:

 0# 0x000055FF74AFB601 in ./boost_stacktrace_header_only.out
 1# 0x000055FF74AFB66C in ./boost_stacktrace_header_only.out
 2# 0x000055FF74AFB69C in ./boost_stacktrace_header_only.out
 3# 0x000055FF74AFB6F7 in ./boost_stacktrace_header_only.out
 4# 0x00007F0176E7BD90 in /lib/x86_64-linux-gnu/libc.so.6
 5# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 6# 0x000055FF74AFB4E5 in ./boost_stacktrace_header_only.out

我们甚至无法使用addr2line。也许我们必须从以下定义中传递一些其他定义:https://www.boost.org/doc/libs/1_80_0/doc/html/stacktrace/configuration_and_build.html

在Ubuntu 22.04上测试。boost 1.74。

Boost boost::stacktrace::safe_dump_to

这是一个有趣的替代boost::stacktrace::stacktrace,因为它以异步信号安全的方式将堆栈跟踪写入文件,这使它成为自动转储堆栈跟踪的良好选择,在段错误时是一个超常见的用例:How to automatically generate a stacktrace when my program crashes

文档位于:https://www.boost.org/doc/libs/1_70_0/doc/html/boost/stacktrace/safe_dump_1_3_38_7_6_2_1_6.html

TODO 让其能够工作。每次看到的都是一堆随机字节。我的尝试:

boost_stacktrace_safe/mystacktrace.h

#include 
#define BOOST_STACKTRACE_LINK
#include 
void print_stacktrace(void) {
    boost::stacktrace::safe_dump_to(0, 1024, STDOUT_FILENO);
}

示例输出:

1[FU1[FU"2[FU}2[FUm1@n10[FU

每次都会发生很大的变化,表明这是随机的内存地址。

在Ubuntu 22.04上测试,boost 1.74.0。

glibc backtrace

这种方法非常通用,因为它自带glibc。文档位于:https://www.gnu.org/software/libc/manual/html_node/Backtraces.html

在Ubuntu 22.04上测试,glibc 2.35。

glibc_backtrace_symbols_fd/mystacktrace.h

#include  /* backtrace, backtrace_symbols_fd */
#include  /* STDOUT_FILENO */
void print_stacktrace(void) {
    size_t size;
    enum Constexpr { MAX_SIZE = 1024 };
    void *array[MAX_SIZE];
    size = backtrace(array, MAX_SIZE);
    backtrace_symbols_fd(array, size, STDOUT_FILENO);
}

使用以下命令进行编译:

g++ -O0 -ggdb3 -Wall -Wextra -pedantic -rdynamic -std=c++11 -o glibc_backtrace_symbols_fd.out main.cpp

使用-rdynamic的示例输出:

./glibc_backtrace_symbols.out(_Z16print_stacktracev+0x47) [0x556e6a131230]
./glibc_backtrace_symbols.out(_Z9my_func_2v+0xd) [0x556e6a1312d6]
./glibc_backtrace_symbols.out(_Z9my_func_1i+0x14) [0x556e6a131306]
./glibc_backtrace_symbols.out(main+0x58) [0x556e6a131361]
/lib/x86_64-linux-gnu/libc.so.6(+0x29d90) [0x7f175e7bdd90]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x80) [0x7f175e7bde40]
./glibc_backtrace_symbols.out(_start+0x25) [0x556e6a131125]

不使用-rdynamic的示例输出:

./glibc_backtrace_symbols_fd_no_rdynamic.out(+0x11f0)[0x556bd40461f0]
./glibc_backtrace_symbols_fd_no_rdynamic.out(+0x123c)[0x556bd404623c]
./glibc_backtrace_symbols_fd_no_rdynamic.out(+0x126c)[0x556bd404626c]
./glibc_backtrace_symbols_fd_no_rdynamic.out(+0x12c7)[0x556bd40462c7]
/lib/x86_64-linux-gnu/libc.so.6(+0x29d90)[0x7f0da2b70d90]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x80)[0x7f0da2b70e40]
./glibc_backtrace_symbols_fd_no_rdynamic.out(+0x10e5)[0x556bd40460e5]

如果想要不使用-rdynamic获得行号,我们可以使用addr2line

addr2line -C -e glibc_backtrace_symbols_fd_no_rdynamic.out 0x11f0 0x123c 0x126c 0x12c7

不幸的是,addr2line不能处理-rdynamic时使用的函数格式中的函数名和偏移量,例如_Z9my_func_2v+0xd

然而,GDB可以:

gdb -nh -batch -ex 'info line *(_Z9my_func_2v+0xd)' -ex 'info line *(_Z9my_func_1i+0x14)' glibc_backtrace_symbols.out
Line 7 of "main.cpp" starts at address 0x12d6 <_Z9my_func_2v+13> and ends at 0x12d9 <_Z9my_func_1d>.
Line 17 of "main.cpp" starts at address 0x1306 <_Z9my_func_1i+20> and ends at 0x1309 .

一个工具可以让这个过程更舒适:

addr2lines() (
  perl -ne '$m = s/(.*).*\(([^)]*)\).*/gdb -nh -q -batch -ex "info line *\2" \1/;print $_ if $m' | bash
)

使用方法:

xsel -b | addr2lines

glibc backtrace_symbols

backtrace_symbols_fd的一个版本,它返回字符串而不是打印到文件句柄。

glibc_backtrace_symbols/mystacktrace.h

#include  /* backtrace, backtrace_symbols */
#include  /* printf */
void print_stacktrace(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]);
    free(strings);
}

带有C++解析的glibcbacktracehack 1:-export-dynamic + dladdr

我找不到一种简单的方法来自动解析glibc backtrace中的C++符号。

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

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

glibc_ldl.cpp

#include      // for dladdr
#include     // for __cxa_demangle
#include 
#include 
#include 
#include 
// 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上测试。

带有C++解析的glibcbacktracehack 2:解析backtrace输出

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

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

TODO使其编译并在此处显示。

GDB脚本

我们也可以使用GDB进行此操作,而无需重新编译,只需使用:如何在GDB中命中某个断点时执行特定操作?

我们为测试设置一个空的回溯函数:

gdb/mystacktrace.h

void print_stacktrace(void) {}

然后使用:

main.gdb

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

我们可以运行:

gdb -nh -batch -x main.gdb --args gdb.out

示例输出:

Temporary breakpoint 1 at 0x11a7: file main.cpp, line 21.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Temporary breakpoint 1, main (argc=1, argv=0x7fffffffc3e8) at main.cpp:21
warning: Source file is more recent than executable.
21          if (argc > 1) {
Breakpoint 2 at 0x555555555151: file gdb/mystacktrace.h, line 1.
#0  print_stacktrace () at gdb/mystacktrace.h:1
#1  0x0000555555555161 in my_func_2 () at main.cpp:6
#2  0x0000555555555191 in my_func_1 (i=1) at main.cpp:16
#3  0x00005555555551ec in main (argc=1, argv=0x7fffffffc3e8) at main.cpp:27
[Inferior 1 (process 165453) exited normally]

以上内容可以通过以下Bash函数更具可用性:

gdbbt() (
  tmpfile=$(mktemp /tmp/gdbbt.XXXXXX)
  fn="$1"
  shift
  printf '%s' "
start
break $fn
commands
  silent
  backtrace
  printf \"\n\"
  continue
end
continue
" > "$tmpfile"
  gdb -nh -batch -x "$tmpfile" -args "$@"
  rm -f "$tmpfile"
)

用法:

gdbbt print_stacktrace gdb.out 2

我不知道如何使用-ex进行commands,而不需要临时文件:在命令行中使用ex命令添加带有命令的断点的问题

在Ubuntu 22.04、GDB 12.0.90下测试通过。

GDB代码注入

TODO这是梦想!它可能允许编译后的速度,但不需要重新编译!任一方式:

libunwind

TODO这是否比glibc backtrace有任何优势?输出非常相似,也需要修改构建命令,但不是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 
#include 
/* Paste this on the file you want to debug. */
#define UNW_LOCAL_ONLY
#include 
#include 
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++名称解析的libunwind

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

unwind.cpp

#define UNW_LOCAL_ONLY
#include 
#include 
#include 
#include 
#include 
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上测试。

Linux内核

如何在Linux内核中打印当前线程的堆栈跟踪?

libdwfl

最初在此处提到:https://stackoverflow.com/a/60713161/895245,它可能是最好的方法,但我必须进行更多的基准测试,请去点赞那个答案。

待办事项: 尝试将那个答案中的代码最小化为一个函数,但它会崩溃,如果有人能找到原因让我知道。

dwfl.cpp:答案达到了30k字符,这是最简单的削减:https://gist.github.com/cirosantilli/f1dd3ee5d324b9d24e40f855723544ac

编译并运行:

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的stacktrace快10倍,因此可能适用于更多的用例。

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

libbacktrace

https://github.com/ianlancetaylor/libbacktrace

考虑到这个库的作者是重度的图书馆作者,值得尝试一下,也许它是The One。待办事项:检查一下。

一个C库,可以链接到C/C++程序中以生成符号回溯

截至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++ unwind API。该API由GCC和clang提供。

另请参阅

0