打印C或C ++中的调用堆栈
打印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。
背景
我有一个测试运行,基于一些命令行开关表现不同,但不应影响该行为。我的代码有一个伪随机数生成器,我假设基于这些开关调用方式不同。我想能够在每组开关下运行测试,并查看每个开关是否会不同地调用伪随机数生成器。
如果你只想用 Linux 的解决方案,你可以使用 backtrace(3),它会简单地返回一个 void *
数组(实际上,每个数组元素都指向相应栈帧的返回地址)。为了将这些转换为有用的东西,可以使用 backtrace_symbols(3)。
注意 backtrace(3) 中的注释部分:
没有使用特殊链接器选项可能导致无法获取符号名称。对于使用 GNU 链接器的系统,必须使用 -rdynamic 链接器选项。请注意,“静态”函数的名称是不公开的,并且不会在回溯中提供。
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透明度”的人来说,它仍然是一个非常好的选择。
在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++解析的glibcbacktrace
hack 1:-export-dynamic
+ dladdr
我找不到一种简单的方法来自动解析glibc backtrace
中的C++符号。
- https://panthema.net/2008/0901-stacktrace-demangled/
- https://gist.github.com/fmela/591333/c64f4eb86037bb237862a8283df70cdfc25f01d3
改编自: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++解析的glibcbacktrace
hack 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这是梦想!它可能允许编译后的速度,但不需要重新编译!任一方式:
- 使用
compile code
+ 其他选项之一,理想情况下是C++23
:如何在gdb中调用汇编?可能已经可以实现。但compile code
非常奇怪,所以我懒得尝试 - 类似于
dprintf
的内置dbt
命令动态printf:如何在GDB中命中某个断点时执行特定操作?
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_2
和my_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内核
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提供。
另请参阅