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

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

void foo()
{
print_stack_trace();


// foo's body


return
}

在那里,print_stack_trace的工作方式与佩尔的 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。


背景资料

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

254406 次浏览

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

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

对于只使用 linux 的解决方案,您可以使用返回 void *数组的 回溯追踪(3)(实际上每个 回溯追踪(3)都指向相应堆栈帧的返回地址)。为了将这些转换成有用的东西,有 反向跟踪 _ 符号(3)

注意 备注部分(3):

符号名称可能不可用 没有使用特殊的连接器 选择。 对于使用 GNU 链接器的系统,有必要使用 - 动力连接器 注意,“ static”函数的名称不会公开, 不会的 可用于反向跟踪。

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

使用全局(字符串)堆栈,在每个函数开始时将函数名和其他值(如参数)推到堆栈上; 在函数退出时再次弹出。

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

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

没有标准化的方法来做到这一点。对于窗口的功能是在 救命库中提供的

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

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

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

如果您希望使用一种不那么麻烦的方式,那么您总是可以使用日志记录。有一些非常有效的日志记录工具,例如 Pantheios。这又一次让你对正在发生的事情有了更准确的了解。

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

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

完整的源代码可以在这里获得。

Https://github.com/nanolat 处的调用栈

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

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

我知道这个帖子有些年头了,但我觉得它对其他人也有用。如果您使用的是 gcc,那么您可以使用它的工具特性(- finstrument- 函数选项)来记录任何函数调用(进入和退出)。更多信息请看: http://hacktalks.blogspot.fr/2013/08/gcc-instrument-functions.html

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

我已经测试过了,它的工作完美,非常方便

更新: 您还可以在 GCC 文档中找到有关-finstruments-function 编译选项的信息,这些选项与 Instruments 选项有关: https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html

又一个旧线索的答案。

当我需要这样做时,我通常只使用 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 或者简单的等价物,但是这个 线程似乎有另一种选择

如果您正在使用 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,基于 这篇文章

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

C/C + + 回溯方法综述

在这个答案中,我将尝试为一系列解决方案运行一个基准测试,以查看哪个解决方案运行得更快,同时还要考虑特性和可移植性等其他问题。

工具 时间/召唤 电话号码 函数名 C + + 解密 重新编译 信号安全
C + + 23 <stacktrace> GCC 12.1 7个我们 N
格利布克 backtrace_symbols_fd 25美元 N -rdynamic 黑客
GDB 脚本 2毫秒 N
GDB 代码注入 N
启动堆栈跟踪 40毫秒
Libunwind
Libdwfl 4毫秒 N
Libbacktrace

空单元格意味着“ TODO”,而不是“ no”。

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

    在使用 addr2line之后,总是可以从地址中手动恢复行号。但这是一种痛苦。

  • 重新编译: 需要重新编译程序来获得你的跟踪。不重新编译更好!

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

C + + 23 <stacktrace>

这个方法之前提到过: https://stackoverflow.com/a/69384663/895245请考虑支持这个答案。

这是最好的解决方案... 它的便携性,快速,显示行号和解除 C + + 符号。一旦这个选项变得更加广泛可用,它将取代所有其他选项,也许只有 GDB 例外,它只是一次性的,不需要或者不需要重新编译。

Stacktrace.cpp

#include <stacktrace>


#include <iostream>


void my_func_2(void) {
std::cout << std::stacktrace::current();
}


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 27
my_func_1(2.0); // line 28
}
}

来自 Ubuntu 22.04的 GCC 12.1.0没有编译支持,所以现在我从源代码构建它,按照: 如何编辑和重建 gCC libstdc + + C++标准程式库源代码?并设置 --enable-libstdcxx-backtrace=yes,它工作了!

编制:

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

基准:

time ./stacktrace.out 100000 &>/tmp/bt.log

结果:

real    0m1.335s
user    0m1.255s
sys     0m0.080s

输出样本:

   0# my_func_2() at /home/ciro/stacktrace.cpp:6
1# my_func_1(int) at /home/ciro/stacktrace.cpp:16
2#      at /home/ciro/stacktrace.cpp:27
3#      at :0
4#      at :0
5#      at :0
6#
0# my_func_2() at /home/ciro/stacktrace.cpp:6
1# my_func_1(double) at /home/ciro/stacktrace.cpp:11
2#      at /home/ciro/stacktrace.cpp:28
3#      at :0
4#      at :0
5#      at :0
6#

如果我们尝试使用 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

书目:

它不会在 include 上失败,因为头文件:

/usr/include/c++/12

有一个功能检查:

#if __cplusplus > 202002L && _GLIBCXX_HAVE_STACKTRACE

启动堆栈跟踪

文件编号: 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 只是头部,因此最有可能的情况是不需要修改您的构建系统

Bostacktrace. 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
}
}

不幸的是,它似乎是最近才加入的,而且在 Ubuntu 16.04中没有包 libboost-stacktrace-dev,只有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 backtrace”小节将进一步解释这个输出,它是类似的。

请注意,my_func_1(int)my_func_1(float)由于功能过载而损坏是如何为我们精心设计的。

请注意,第一个 int调用关闭了一行(28行而不是27行,第二个 int调用关闭了两行(27行而不是29行)。之所以是 在评论中建议,是因为考虑了以下指令地址,这使得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

回溯通常被优化不可挽回地破坏。尾部呼叫优化就是一个显著的例子: 什么是尾部调用优化?

-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,升级1.67.0上测试。

推进 boost::stacktrace::safe_dump_to

这是 boost::stacktrace::stacktrace的一个有趣的替代方案,boost::stacktrace::stacktrace以异步信号安全的方式将堆栈跟踪保存到一个文件中,这使得它成为一个很好的选项,可以自动将堆栈跟踪转储到 Segfault 上,这是一个非常常见的用例: 当程序崩溃时如何自动生成堆栈跟踪

格利布克 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 + + 符号,但是,这里有一些技巧:

在 Ubuntu 16.04,GCC 6.4.0,libc 2.23上测试。

格利布克 backtrace_symbols_fd

这个 helper 比 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

这是一个“黑客”,因为它需要改变与 -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上测试。

用 C + + deangling hack 2解析 backtrace: 解析回溯输出

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

这是一种黑客行为,因为它需要解析。

TODO 让它编译并在这里显示。

Libunwind

TODO 比 glibc 回溯有什么优势吗?非常相似的输出,也需要修改 build 命令,但不是 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:

跑步:

./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 <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 中命中?在不重新编译的情况下使用 GDB 来实现这一点

Gdb.cpp

#include <iostream>


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(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 21
my_func_1(2.0); // line 22
}
}

编译并运行:

g++  -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o gdb.out gdb.cpp
time gdb -nh -batch -x main.gdb --args gdb.out 1000 &>/tmp/bt.log

结果:

real    0m1.608s
user    0m1.240s
sys     0m0.430s

输出样本:

#0  my_func_2 () at gdb.cpp:3
#1  0x00005555555551c1 in my_func_1 (i=1) at gdb.cpp:10
#2  0x000055555555521c in main (argc=2, argv=0x7fffffffcd68) at gdb.cpp:21


#0  my_func_2 () at gdb.cpp:3
#1  0x00005555555551aa in my_func_1 (f=2) at gdb.cpp:6
#2  0x000055555555522d in main (argc=2, argv=0x7fffffffcd68) at gdb.cpp:22


[Inferior 1 (process 824882) exited normally]

TODO 我想用命令行中的 -ex来做这件事,这样就不必创建 main.gdb,但是我不能让 commands在那里工作。

在 Ubuntu 19.04,GDB 8.2中测试。

GDB 代码注入

TODO 这是一个梦想! 它可能同时支持类似编译的速度,但是不需要重新编译:

Linux 内核

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

Libdwfl

这是最初提到在: https://stackoverflow.com/a/60713161/895245,它可能是最好的方法,但我必须基准多一点,但请去支持这个答案。

待办事项: 我试图最小化这个答案中的代码,这是工作,到一个单一的函数,但它是分段,让我知道,如果有人可以找到为什么。

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

考虑到哈克图书馆的作者,这是值得一试的,也许它是一个。

一种可以链接到 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。

参见

Linux 特有的 TLDR:

  1. glibc中的 backtrace 仅当 -lunwind被链接时才产生准确的堆栈跟踪(未记录的特定于平台的特性)。
  2. 要输出 函数名源文件电话号码使用 #include <elfutils/libdwfl.h>(这个库只在头文件中有文档说明)。backtrace_symbolsbacktrace_symbolsd_fd的信息量最小。

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

遗憾的是,伴随的 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为时已晚,这就是为什么必须在由函数 __cxa_throw实现的 throw上捕获堆栈。请注意,在一个多线程程序中,多个线程可以同时调用 __cxa_throw,因此如果它将堆栈跟踪捕获到一个必须是 thread_local的全局数组中,就可以调用 __cxa_throw

您还可以使用栈跟踪打印功能 异步信号安全装置,这样您就可以直接从 SIGSEGVSIGBUS信号处理程序调用它(它们应该使用自己的栈来提高鲁棒性)。使用 libdwfl从信号处理程序获取 函数名源文件电话号码可能会失败,因为它不是异步信号安全的,或者如果进程的地址空间已经严重损坏,但实际上它成功的几率是99% (我从未见过它失败)。


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

  1. throw上的堆栈跟踪捕获到特定于线程的存储中。
  2. 对未处理的异常自动打印堆栈跟踪。
  3. 以异步信号安全的方式打印堆栈跟踪。
  4. 提供一个健壮的信号处理函数,它使用自己的堆栈,以异步信号安全的方式打印堆栈跟踪。用户可以将此功能作为 SIGSEGVSIGBUSSIGFPE等的信号处理程序安装。.
  5. 信号处理程序也可以从 ucontext_t信号函数参数(可能不包括向量寄存器) ,a-la Linux内核oops 日志消息中打印出故障点的所有 CPU 寄存器的值。

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

#include <stacktrace>


/* ... */


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

详情:
https://en.cppreference.com/w/cpp/header/stacktrace
• href = “ https://en.cpferences ence.com/w/cpp/Utility/basic _ stacktrace/Operator _ ltlt”rel = “ norefrer”> https://en.cppreference.com/w/cpp/utility/basic_stacktrace/operator_ltlt