如何使用带有行号信息的 gcc 获取 C++ 的堆栈跟踪?

Posted

技术标签:

【中文标题】如何使用带有行号信息的 gcc 获取 C++ 的堆栈跟踪?【英文标题】:How to get a stack trace for C++ using gcc with line number information? 【发布时间】:2011-06-05 21:45:33 【问题描述】:

我们在专有的assert 中使用堆栈跟踪来捕捉开发人员的错误 - 当发现错误时,会打印堆栈跟踪。

我发现 gcc 的对 backtrace()/backtrace_symbols() 方法不足:

    名字被打乱了 没有线路信息

第一个问题可以通过abi::__cxa_demangle解决。

但是第二个问题更难。我找到了replacement for backtrace_symbols()。 这比 gcc 的 backtrace_symbols() 更好,因为它可以检索行号(如果使用 -g 编译)并且您不需要使用 -rdynamic 进行编译。

悬停代码是 GNU 许可的,所以恕我直言,我不能在商业代码中使用它。

有什么建议吗?

附言

gdb 能够打印出传递给函数的参数。 可能要求已经太多了:)

PS 2

Similar question(感谢 nobar)

【问题讨论】:

要么找到作者并付钱给他,要么自己重新实现。 我不确定在您的商业应用程序上使用已编译的 GNU 代码是否与修改/自定义 GNU 代码本身以在您的应用程序中分发相同。有人吗? 仅适用于 Linux/x86 还是您应该在不同平台上运行此代码? 无行号要求:***.com/questions/3899870/print-call-stack-in-c-or-c 【参考方案1】:

因此,您需要一个打印堆栈跟踪的独立函数,该函数具有 gdb 堆栈跟踪所具有的所有功能,并且不会终止您的应用程序。答案是以非交互模式自动启动 gdb 以执行您想要的任务。

这是通过在子进程中执行 gdb、使用 fork() 并编写脚本以在应用程序等待它完成时显示堆栈跟踪来完成的。这可以在不使用核心转储且不中止应用程序的情况下执行。我通过查看这个问题学会了如何做到这一点:How it's better to invoke gdb from program to print it's stacktrace?

与该问题一起发布的示例对我来说并没有完全按照所写的那样工作,所以这是我的“固定”版本(我在 Ubuntu 9.04 上运行)。

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/prctl.h>

void print_trace() 
    char pid_buf[30];
    sprintf(pid_buf, "%d", getpid());
    char name_buf[512];
    name_buf[readlink("/proc/self/exe", name_buf, 511)]=0;
    prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0);
    int child_pid = fork();
    if (!child_pid) 
        dup2(2,1); // redirect output to stderr - edit: unnecessary?
        execl("/usr/bin/gdb", "gdb", "--batch", "-n", "-ex", "thread", "-ex", "bt", name_buf, pid_buf, NULL);
        abort(); /* If gdb failed to start */
     else 
        waitpid(child_pid,NULL,0);
    

如引用的问题所示,gdb 提供了您可以使用的其他选项。例如,使用“bt full”而不是“bt”会生成更详细的报告(局部变量包含在输出中)。 gdb 的联机帮助页很简单,但可以使用完整的文档here。

由于这是基于 gdb,因此输出包括 解构名称行号函数参数,甚至还有可选的 局部变量。此外,gdb 是线程感知的,因此您应该能够提取一些特定于线程的元数据。

这是我使用此方法看到的堆栈跟踪类型的示例。

0x00007f97e1fc2925 in waitpid () from /lib/libc.so.6
[Current thread is 0 (process 15573)]
#0  0x00007f97e1fc2925 in waitpid () from /lib/libc.so.6
#1  0x0000000000400bd5 in print_trace () at ./demo3b.cpp:496
2  0x0000000000400c09 in recursive (i=2) at ./demo3b.cpp:636
3  0x0000000000400c1a in recursive (i=1) at ./demo3b.cpp:646
4  0x0000000000400c1a in recursive (i=0) at ./demo3b.cpp:646
5  0x0000000000400c46 in main (argc=1, argv=0x7fffe3b2b5b8) at ./demo3b.cpp:70

注意:我发现这与 valgrind 的使用不兼容(可能是由于 Valgrind 使用了虚拟机)。当您在 gdb 会话中运行程序时,它也不起作用(无法将“ptrace”的第二个实例应用于进程)。

【讨论】:

不要使用这个!我在我的程序中逐字使用了上述函数,在 Ubuntu 12.04 上它完全使 X 服务器崩溃。 @BeniBela,你运行的是什么程序?是低级的吗?这种方法在 Fedora 17 中适用于我。 而且情况越来越糟:ptracing parent 现在不再是permitted。但也许您可以使用prctl 设置一个标志? @BeniBela:感谢您的指点。一种可能的解决方法是使用sudo 运行。 你可以在fork()之前使用#include &lt;sys/prctl.h&gt;prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0);绕过它。【参考方案2】:

不久前I answered a similar question。您应该查看方法 #4 上可用的源代码,它还打印行号和文件名。

方法#4:

我对方法 #3 进行了一个小改进,以打印行号。这也可以复制到方法 #2 上。

基本上,它使用 addr2line 将地址转换为文件名和行号。

下面的源代码打印所有本地函数的行号。如果调用另一个库中的函数,您可能会看到几个 ??:0 而不是文件名。

#include <stdio.h>
#include <signal.h>
#include <stdio.h>
#include <signal.h>
#include <execinfo.h>

void bt_sighandler(int sig, struct sigcontext ctx) 

  void *trace[16];
  char **messages = (char **)NULL;
  int i, trace_size = 0;

  if (sig == SIGSEGV)
    printf("Got signal %d, faulty address is %p, "
           "from %p\n", sig, ctx.cr2, ctx.eip);
  else
    printf("Got signal %d\n", sig);

  trace_size = backtrace(trace, 16);
  /* overwrite sigaction with caller's address */
  trace[1] = (void *)ctx.eip;
  messages = backtrace_symbols(trace, trace_size);
  /* skip first stack frame (points here) */
  printf("[bt] Execution path:\n");
  for (i=1; i<trace_size; ++i)
  
    printf("[bt] #%d %s\n", i, messages[i]);

    /* find first occurence of '(' or ' ' in message[i] and assume
     * everything before that is the file name. (Don't go beyond 0 though
     * (string terminator)*/
    size_t p = 0;
    while(messages[i][p] != '(' && messages[i][p] != ' '
            && messages[i][p] != 0)
        ++p;

    char syscom[256];
    sprintf(syscom,"addr2line %p -e %.*s", trace[i], p, messages[i]);
        //last parameter is the file name of the symbol
    system(syscom);
  

  exit(0);



int func_a(int a, char b) 

  char *p = (char *)0xdeadbeef;

  a = a + b;
  *p = 10;  /* CRASH here!! */

  return 2*a;



int func_b() 

  int res, a = 5;

  res = 5 + func_a(a, 't');

  return res;



int main() 

  /* Install our signal handler */
  struct sigaction sa;

  sa.sa_handler = (void *)bt_sighandler;
  sigemptyset(&sa.sa_mask);
  sa.sa_flags = SA_RESTART;

  sigaction(SIGSEGV, &sa, NULL);
  sigaction(SIGUSR1, &sa, NULL);
  /* ... add any other signal here */

  /* Do something */
  printf("%d\n", func_b());

这段代码应该编译为:gcc sighandler.c -o sighandler -rdynamic

程序输出:

Got signal 11, faulty address is 0xdeadbeef, from 0x8048975
[bt] Execution path:
[bt] #1 ./sighandler(func_a+0x1d) [0x8048975]
/home/karl/workspace/stacktrace/sighandler.c:44
[bt] #2 ./sighandler(func_b+0x20) [0x804899f]
/home/karl/workspace/stacktrace/sighandler.c:54
[bt] #3 ./sighandler(main+0x6c) [0x8048a16]
/home/karl/workspace/stacktrace/sighandler.c:74
[bt] #4 /lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe6) [0x3fdbd6]
??:0
[bt] #5 ./sighandler() [0x8048781]
??:0

【讨论】:

记得用-rdynamic编译你的应用程序。 @karlphillip,关于 GPL。如果 GPL(不是 GNU,而是 GPL 许可)代码(使用 ld.so 或 ld)链接到另一个代码,则 GPL 要求另一个代码在 GPL 下可用。这一切仅在应用程序转移给其他人的情况下才是正确的。就个人而言,您可以使用 GPL 代码做任何事情并将其与任何内容链接。 @osgx 如果您的应用程序使用系统目标上可用的动态链接 GPL 库会发生什么。同样的规则? @dimba:别忘了给他赏金(就在接受标记的下面!!) 错误:“struct sigcontext”没有名为“eip”的成员;你的意思是“撕裂”吗?【参考方案3】:

How to generate a stacktrace when my gcc C++ app crashes 对基本相同的问题进行了激烈的讨论。提供了许多建议,包括关于如何在运行时生成堆栈跟踪的大量讨论。

来自该线程的My personal favorite answer 用于启用core dumps,它允许您查看崩溃时的完整应用程序状态(包括函数参数、行号和未损坏的名称)。这种方法的另一个好处是它不仅适用于断言,还适用于分段错误未处理的异常

不同的 Linux shell 使用不同的命令来启用核心转储,但您可以在应用程序代码中使用类似这样的方式来实现...

#include <sys/resource.h>
...
struct rlimit core_limit =  RLIM_INFINITY, RLIM_INFINITY ;
assert( setrlimit( RLIMIT_CORE, &core_limit ) == 0 ); // enable core dumps for debug builds

崩溃后,运行您喜欢的调试器来检查程序状态。

$ kdbg executable core

这是一些示例输出...

还可以在命令行从核心转储中提取堆栈跟踪。

$ ( CMDFILE=$(mktemp); echo "bt" >$CMDFILE; gdb 2>/dev/null --batch -x $CMDFILE temp.exe core )
Core was generated by `./temp.exe'.
Program terminated with signal 6, Aborted.
[New process 22857]
#0  0x00007f4189be5fb5 in raise () from /lib/libc.so.6
#0  0x00007f4189be5fb5 in raise () from /lib/libc.so.6
#1  0x00007f4189be7bc3 in abort () from /lib/libc.so.6
#2  0x00007f4189bdef09 in __assert_fail () from /lib/libc.so.6
#3  0x00000000004007e8 in recursive (i=5) at ./demo1.cpp:18
#4  0x00000000004007f3 in recursive (i=4) at ./demo1.cpp:19
#5  0x00000000004007f3 in recursive (i=3) at ./demo1.cpp:19
#6  0x00000000004007f3 in recursive (i=2) at ./demo1.cpp:19
#7  0x00000000004007f3 in recursive (i=1) at ./demo1.cpp:19
#8  0x00000000004007f3 in recursive (i=0) at ./demo1.cpp:19
#9  0x0000000000400849 in main (argc=1, argv=0x7fff2483bd98) at ./demo1.cpp:26

【讨论】:

gdb 用于死后分析。我正在寻找更多如何从内部代码接收信息。也许我不想在 SIGSEV 的情况下打印回溯 - 例如查看未处理的 C++ 异常是从哪里引发的。 未处理的异常将生成一个核心转储,您可以在抛出时使用它来分析堆栈——所以这个答案适用于此。 另一方面,如果您想在不终止程序的情况下生成堆栈跟踪,我发布了另一个解决该要求的答案。 如果你有构建在外星系统上的二进制文件(来自开源代码),那就不好了,你无法理解你身边的核心转储。而且你不能要求某些用户运行这样的命令。【参考方案4】:

由于 GPL 许可代码旨在帮助您进行开发,因此您不能将其包含在最终产品中。 GPL 限制您分发与非 GPL 兼容代码链接的 GPL 许可代码。只要你只在内部使用 GPL 代码就可以了。

【讨论】:

动态链接可能(几乎可以肯定)没问题,因为源仍然是开放的。断言和分析进入可执行文件的代码...@Keith 是对的,在内部使用它。 据我所知,动态链接尚未在法庭上进行过测试。虽然 FSF 认为这是不允许的,但其他人有不同的看法。***对此有很好的讨论。 en.wikipedia.org/wiki/…【参考方案5】:

为此使用 google glog 库。它具有新的 BSD 许可证。

它在 stacktrace.h 文件中包含一个 GetStackTrace 函数。

编辑

我在这里http://blog.bigpixel.ro/2010/09/09/stack-unwinding-stack-trace-with-gcc/ 发现有一个名为 addr2line 的实用程序,可以将程序地址转换为文件名和行号。

http://linuxcommand.org/man_pages/addr2line1.html

【讨论】:

OP 要求的是堆栈跟踪,而不是日志记录。 Glog 具有堆栈跟踪打印功能 确实 glog 有堆栈跟踪(google-glog.googlecode.com/svn/trunk/doc/glog.htmlFailure Signal Handler 部分),但它没有代码行信息。 我没有时间尝试 addr2line 但它可以成为一个解决方案 google-glog 是对 backtrace 和 backtrace_symbols 的精简包装。它不会给你文件名和行号【参考方案6】:

这是另一种方法。 debug_assert() 宏以编程方式设置条件断点。如果您在调试器中运行,您将在断言表达式为 false 时遇到断点——并且您可以分析实时堆栈(程序不会终止)。如果您没有在调试器中运行,失败的 debug_assert() 会导致程序中止,并且您会得到一个核心转储,您可以从中分析堆栈(请参阅我之前的回答)。

与普通断言相比,这种方法的优点是您可以在触发 debug_assert 后(在调试器中运行时)继续运行程序。换句话说,debug_assert() 比 assert() 稍微灵活一些。

   #include <iostream>
   #include <cassert>
   #include <sys/resource.h> 

// note: The assert expression should show up in
// stack trace as parameter to this function
void debug_breakpoint( char const * expression )
   
   asm("int3"); // x86 specific
   

#ifdef NDEBUG
   #define debug_assert( expression )
#else
// creates a conditional breakpoint
   #define debug_assert( expression ) \
      do  if ( !(expression) ) debug_breakpoint( #expression );  while (0)
#endif

void recursive( int i=0 )
   
   debug_assert( i < 5 );
   if ( i < 10 ) recursive(i+1);
   

int main( int argc, char * argv[] )
   
   rlimit core_limit =  RLIM_INFINITY, RLIM_INFINITY ;
   setrlimit( RLIMIT_CORE, &core_limit ); // enable core dumps
   recursive();
   

注意:有时在调试器中设置“条件断点”可能会很慢。通过以编程方式建立断点,该方法的性能应该与普通 assert() 的性能相当。

注意:正如所写,这是特定于 Intel x86 架构的——其他处理器可能有不同的指令来生成断点。

【讨论】:

我曾经使用过类似的东西,但使用的是空函数 debug_breakpoint。调试时,我只是在 gdb 提示符下输入“bre debug_breakpoint” - 不需要汇编器(在单独的编译单元中编译 debug_breakpoint 以避免优化调用)。【参考方案7】:

有点晚了,但是您可以使用libbfb 来获取文件名和行号,就像 refdbg 在symsnarf.c 中所做的那样。 addr2linegdb 内部使用 libbfb

【讨论】:

【参考方案8】:

这是我的解决方案:

#include <execinfo.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <iostream>
#include <zconf.h>
#include "regex"

std::string getexepath() 
    char result[PATH_MAX];
    ssize_t count = readlink("/proc/self/exe", result, PATH_MAX);
    return std::string(result, (count > 0) ? count : 0);


std::string sh(std::string cmd) 
    std::array<char, 128> buffer;
    std::string result;
    std::shared_ptr<FILE> pipe(popen(cmd.c_str(), "r"), pclose);
    if (!pipe) throw std::runtime_error("popen() failed!");
    while (!feof(pipe.get())) 
        if (fgets(buffer.data(), 128, pipe.get()) != nullptr) 
            result += buffer.data();
        
    
    return result;



void print_backtrace(void) 
    void *bt[1024];
    int bt_size;
    char **bt_syms;
    int i;

    bt_size = backtrace(bt, 1024);
    bt_syms = backtrace_symbols(bt, bt_size);
    std::regex re("\\[(.+)\\]");
    auto exec_path = getexepath();
    for (i = 1; i < bt_size; i++) 
        std::string sym = bt_syms[i];
        std::smatch ms;
        if (std::regex_search(sym, ms, re)) 
            std::string addr = ms[1];
            std::string cmd = "addr2line -e " + exec_path + " -f -C " + addr;
            auto r = sh(cmd);
            std::regex re2("\\n$");
            auto r2 = std::regex_replace(r, re2, "");
            std::cout << r2 << std::endl;
        
    
    free(bt_syms);


void test_m() 
    print_backtrace();


int main() 
    test_m();
    return 0;

输出:

/home/roroco/Dropbox/c/ro-c/cmake-build-debug/ex/test_backtrace_with_line_number
test_m()
/home/roroco/Dropbox/c/ro-c/ex/test_backtrace_with_line_number.cpp:57
main
/home/roroco/Dropbox/c/ro-c/ex/test_backtrace_with_line_number.cpp:61
??
??:0

“??”和 "??:0" 因为这个跟踪是在 libc 中,而不是在我的源代码中

【讨论】:

【参考方案9】:

解决方案之一是在失败的断言处理程序中使用“bt”脚本启动 gdb。集成这样的 gdb 启动不是很容易,但它会给你 backtrace 和 args 和 demangle 名称(或者你可以通过 c++filt 程序传递 gdb 输出)。

这两个程序(gdb 和 c++filt)都不会链接到您的应用程序中,因此 GPL 不会要求您开源完整的应用程序。

您可以对回溯符号使用相同的方法(执行 GPL 程序)。只需生成 %eip 的 ascii 列表和 exec 文件的映射 (/proc/self/maps) 并将其传递给单独的二进制文件。

【讨论】:

【参考方案10】:

您可以使用DeathHandler - 小型 C++ 类,它为您完成所有工作,可靠。

【讨论】:

【参考方案11】:

我想行号与当前的 eip 值有关,对吧?

解决方案 1: 然后你可以使用GetThreadContext() 之类的东西,除非你是在 linux 上工作。我用谷歌搜索了一下,发现了类似的东西,ptrace():

ptrace() 系统调用提供了一个 父进程可以通过何种方式 观察和控制执行情况 另一个过程,并检查和 更改其核心图像和寄存器。 [...] 父母可以通过以下方式启动跟踪 调用 fork(2) 并拥有 结果孩子做了一个 PTRACE_TRACEME, 后面(通常)是一个 exec(3)。 或者,父母可以开始 使用现有进程的跟踪 PTRACE_ATTACH。

现在我在想,你可以做一个“主”程序来检查发送给它的子程序的信号,即你正在处理的真正程序。在fork() 之后它调用waitid():

所有这些系统调用都用于 等待一个孩子的状态变化 调用进程,并获取 关于孩子的信息 状态已经改变。

如果 SIGSEGV(或类似的东西)被捕获,则调用 ptrace() 以获取 eip 的值。

PS:我从来没有使用过这些系统调用(实际上,我以前从未见过它们;)所以我不知道这是否可能,两者都不能帮助你。至少我希望这些链接有用。 ;)

解决方案 2: 第一个解决方案相当复杂,对吧?我想出了一个更简单的方法:使用signal() 捕获您感兴趣的信号并调用一个简单的函数来读取存储在堆栈中的eip 值:

...
signal(SIGSEGV, sig_handler);
...

void sig_handler(int signum)

    int eip_value;

    asm 
        push eax;
        mov eax, [ebp - 4]
        mov eip_value, eax
        pop eax
    

    // now you have the address of the
    // **next** instruction after the
    // SIGSEGV was received

那个 asm 语法是 Borland 的语法,只要把它改成 GAS。 ;)

【讨论】:

您的“帖子”中的解决方案为零。这看起来不像是一个答案 @osgx:你能解释一下原因吗?这些解决方案可以为您提供发生错误的位置。 这不是完整的解决方案。很明显,从外部获取二进制信息是ptrace。显然是从堆栈中取出一个eip。但是您没有给出任何答案如何获取整个回溯并将其转换为函数名称和源文件行信息。 @osgx:我很想知道你是如何从“这不是一个完整的解决方案”到“你的帖子中的解决方案为零”的说法。部分解决方案在 SO 上既可接受又有价值。 @BlackBear - 已经有这样的日志了。每个函数都必须知道它运行后要返回的地址。该日志称为函数调用堆栈。 yosefk.com/blog/… - 有一个例子,它是如何从堆栈中获取的。【参考方案12】:

这是我的第三个答案——仍在尝试利用核心转储。

“类断言”宏是应该终止应用程序(断言的方式)还是应该在生成堆栈跟踪后继续执行,这一问题并不完全清楚。

在这个答案中,我正在解决您想要显示堆栈跟踪并继续执行的情况。我在下面编写了 coredump() 函数来生成核心转储,自动从中提取堆栈跟踪,然后继续执行程序。

用法同assert()。当然,不同之处在于 assert() 会终止程序,而 coredump_assert() 不会。

   #include <iostream>
   #include <sys/resource.h> 
   #include <cstdio>
   #include <cstdlib>
   #include <boost/lexical_cast.hpp>
   #include <string>
   #include <sys/wait.h>
   #include <unistd.h>

   std::string exename;

// expression argument is for diagnostic purposes (shows up in call-stack)
void coredump( char const * expression )
   

   pid_t childpid = fork();

   if ( childpid == 0 ) // child process generates core dump
      
      rlimit core_limit =  RLIM_INFINITY, RLIM_INFINITY ;
      setrlimit( RLIMIT_CORE, &core_limit ); // enable core dumps
      abort(); // terminate child process and generate core dump
      

// give each core-file a unique name
   if ( childpid > 0 ) waitpid( childpid, 0, 0 );
   static int count=0;
   using std::string;
   string pid = boost::lexical_cast<string>(getpid());
   string newcorename = "core-"+boost::lexical_cast<string>(count++)+"."+pid;
   string rawcorename = "core."+boost::lexical_cast<string>(childpid);
   int rename_rval = rename(rawcorename.c_str(),newcorename.c_str()); // try with core.PID
   if ( rename_rval == -1 ) rename_rval = rename("core",newcorename.c_str()); // try with just core
   if ( rename_rval == -1 ) std::cerr<<"failed to capture core file\n";

  #if 1 // optional: dump stack trace and delete core file
   string cmd = "( CMDFILE=$(mktemp); echo 'bt' >$CMDFILE; gdb 2>/dev/null --batch -x $CMDFILE "+exename+" "+newcorename+" ; unlink $CMDFILE )";
   int system_rval = system( ("bash -c '"+cmd+"'").c_str() );
   if ( system_rval == -1 ) std::cerr.flush(), perror("system() failed during stack trace"), fflush(stderr);
   unlink( newcorename.c_str() );
  #endif

   

#ifdef NDEBUG
   #define coredump_assert( expression ) ((void)(expression))
#else
   #define coredump_assert( expression ) do  if ( !(expression) )  coredump( #expression );   while (0)
#endif

void recursive( int i=0 )
   
   coredump_assert( i < 2 );
   if ( i < 4 ) recursive(i+1);
   

int main( int argc, char * argv[] )
   
   exename = argv[0]; // this is used to generate the stack trace
   recursive();
   

当我运行程序时,它会显示三个堆栈跟踪...

Core was generated by `./temp.exe'.                                         
Program terminated with signal 6, Aborted.
[New process 24251]
#0  0x00007f2818ac9fb5 in raise () from /lib/libc.so.6
#0  0x00007f2818ac9fb5 in raise () from /lib/libc.so.6
#1  0x00007f2818acbbc3 in abort () from /lib/libc.so.6
#2  0x0000000000401a0e in coredump (expression=0x403303 "i < 2") at ./demo3.cpp:29
#3  0x0000000000401f5f in recursive (i=2) at ./demo3.cpp:60
#4  0x0000000000401f70 in recursive (i=1) at ./demo3.cpp:61
#5  0x0000000000401f70 in recursive (i=0) at ./demo3.cpp:61
#6  0x0000000000401f8b in main (argc=1, argv=0x7fffc229eb98) at ./demo3.cpp:66
Core was generated by `./temp.exe'.
Program terminated with signal 6, Aborted.
[New process 24259]
#0  0x00007f2818ac9fb5 in raise () from /lib/libc.so.6
#0  0x00007f2818ac9fb5 in raise () from /lib/libc.so.6
#1  0x00007f2818acbbc3 in abort () from /lib/libc.so.6
#2  0x0000000000401a0e in coredump (expression=0x403303 "i < 2") at ./demo3.cpp:29
#3  0x0000000000401f5f in recursive (i=3) at ./demo3.cpp:60
#4  0x0000000000401f70 in recursive (i=2) at ./demo3.cpp:61
#5  0x0000000000401f70 in recursive (i=1) at ./demo3.cpp:61
#6  0x0000000000401f70 in recursive (i=0) at ./demo3.cpp:61
#7  0x0000000000401f8b in main (argc=1, argv=0x7fffc229eb98) at ./demo3.cpp:66
Core was generated by `./temp.exe'.
Program terminated with signal 6, Aborted.
[New process 24267]
#0  0x00007f2818ac9fb5 in raise () from /lib/libc.so.6
#0  0x00007f2818ac9fb5 in raise () from /lib/libc.so.6
#1  0x00007f2818acbbc3 in abort () from /lib/libc.so.6
#2  0x0000000000401a0e in coredump (expression=0x403303 "i < 2") at ./demo3.cpp:29
#3  0x0000000000401f5f in recursive (i=4) at ./demo3.cpp:60
#4  0x0000000000401f70 in recursive (i=3) at ./demo3.cpp:61
#5  0x0000000000401f70 in recursive (i=2) at ./demo3.cpp:61
#6  0x0000000000401f70 in recursive (i=1) at ./demo3.cpp:61
#7  0x0000000000401f70 in recursive (i=0) at ./demo3.cpp:61
#8  0x0000000000401f8b in main (argc=1, argv=0x7fffc229eb98) at ./demo3.cpp:66

【讨论】:

从应用程序调用system()时,你不怕一些副作用吗?就我而言,我说的是消耗合理数量的常驻内存的多线程应用程序。 这里对于多线程程序有一个问题:在写入和重命名核心文件时存在竞争条件——两个不同的线程可能同时使用相同的核心文件名。你可以添加一个互斥锁来解决这个问题。 @dimba:您能否更具体地说明调用 system() 时可能出现的副作用种类?我不知道有任何此类问题。【参考方案13】:

我必须在有很多限制的生产环境中执行此操作,所以我想解释一下已经发布的方法的优缺点。

    附加 GDB

+ 非常简单和健壮

- 对于大型程序来说很慢,因为 GDB 坚持将整个地址加载到第 # 行数据库中而不是延迟

- 干扰信号处理。 GDB在附加的时候,拦截了SIGINT(ctrl-c)之类的信号,会导致程序卡在GDB交互提示?如果某个其他进程定期发送此类信号。也许有一些方法可以解决它,但这使得 GDB 在我的情况下无法使用。如果您只关心在程序崩溃时打印一次调用堆栈,而不是多次打印,您仍然可以使用它。

    addr2 行。这是不使用 backtrace_symbols 的替代解决方案。

+ 不从堆中分配,这在信号处理程序中是不安全的

+ 不需要解析 backtrace_symbols 的输出

- 无法在没有 dladdr1 的 MacOS 上运行。您可以改用 _dyld_get_image_vmaddr_slide,它返回与 link_map::l_addr 相同的偏移量。

- 需要添加负偏移量,否则翻译后的行号将大 1。 backtrace_symbols 为你做这个

#include <execinfo.h>
#include <link.h>
#include <stdlib.h>
#include <stdio.h>

// converts a function's address in memory to its VMA address in the executable file. VMA is what addr2line expects
size_t ConvertToVMA(size_t addr)

  Dl_info info;
  link_map* link_map;
  dladdr1((void*)addr,&info,(void**)&link_map,RTLD_DL_LINKMAP);
  return addr-link_map->l_addr;


void PrintCallStack()

  void *callstack[128];
  int frame_count = backtrace(callstack, sizeof(callstack)/sizeof(callstack[0]));
  for (int i = 0; i < frame_count; i++)
  
    char location[1024];
    Dl_info info;
    if(dladdr(callstack[i],&info))
    
      char command[256];
      size_t VMA_addr=ConvertToVMA((size_t)callstack[i]);
      //if(i!=crash_depth)
        VMA_addr-=1;    // https://***.com/questions/11579509/wrong-line-numbers-from-addr2line/63841497#63841497
      snprintf(command,sizeof(command),"addr2line -e %s -Ci %zx",info.dli_fname,VMA_addr);
      system(command);
    
  


void Foo()

  PrintCallStack();


int main()

  Foo();
  return 0;

我还想澄清 backtrace 和 backtrace_symbols 生成什么地址以及 addr2line 期望什么。 addr2line 需要 FooVMA,或者如果您使用 --section=.text,则需要 Foofile - textfile。回溯返回 Foomem。 backtrace_symbols 在某处生成 FooVMA。 我在其他几篇文章中犯的一个大错误是假设 VMAbase = 0 或 FooVMA = Foofile = Foomem - ELFmem,易于计算。 这通常有效,但对于某些编译器(即链接器脚本)使用 VMAbase > 0。例如 Ubuntu 16 (0x400000) 上的 GCC 5.4 和 MacOS (0x100000000) 上的 clang 11。 对于共享库,它始终为 0。似乎 VMAbase 仅对非位置无关代码有意义。否则,它不会影响 EXE 在内存中的加载位置。

此外,karlphillip 和这个都不需要使用 -rdynamic 进行编译。这将增加二进制大小,尤其是对于大型 C++ 程序或共享库,动态符号表中的无用条目永远不会被导入

【讨论】:

您能否阐明我如何使用 _dyld_get_image_vmaddr_slide 在 mac 上完成这项工作? @michaelsnowden 当然,我已经说过 _dyld_get_image_vmaddr_slide 返回模块在内存中的实际基地址与 VMAbase 之间的差异,或者图片中的 Elf_mem - VMA_base = Foo_mem - Foo_VMA。所以在 ConvertToVMA 中,只需返回 addr - _dyld_get_image_vmaddr_slide (module_index)。我不知道找到地址属于哪个模块的简单方法,所以我最终假设它始终为 0,即主 EXE。【参考方案14】:

目前提供的所有 AFAICS 解决方案都不会打印共享库中的函数名称和行号。这就是我所需要的,所以我更改了 karlphillip 的解决方案(以及类似问题的其他答案),以使用 /proc/id/maps 解析共享库地址。

#include <stdlib.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <execinfo.h>
#include <stdbool.h>

struct Region  // one mapped file, for example a shared library
    uintptr_t start;
    uintptr_t end;
    char* path;
;

static struct Region* getRegions(int* size)  
// parse /proc/self/maps and get list of mapped files 
    FILE* file;
    int allocated = 10;
    *size = 0;
    struct Region* res;
    uintptr_t regionStart = 0x00000000;
    uintptr_t regionEnd = 0x00000000;
    char* regionPath = "";
    uintmax_t matchedStart;
    uintmax_t matchedEnd;
    char* matchedPath;

    res = (struct Region*)malloc(sizeof(struct Region) * allocated);
    file = fopen("/proc/self/maps", "r");
    while (!feof(file)) 
        fscanf(file, "%jx-%jx %*s %*s %*s %*s%*[ ]%m[^\n]\n",  &matchedStart, &matchedEnd, &matchedPath);
        bool bothNull = matchedPath == 0x0 && regionPath == 0x0;
        bool similar = matchedPath && regionPath && !strcmp(matchedPath, regionPath);
        if(bothNull || similar) 
            free(matchedPath);
            regionEnd = matchedEnd;
         else 
            if(*size == allocated) 
                allocated *= 2;
                res = (struct Region*)realloc(res, sizeof(struct Region) * allocated);
            

            res[*size].start = regionStart;
            res[*size].end = regionEnd;
            res[*size].path = regionPath;
            (*size)++;
            regionStart = matchedStart;
            regionEnd = matchedEnd;
            regionPath = matchedPath;
        
    
    return res;


struct SemiResolvedAddress 
    char* path;
    uintptr_t offset;
;
static struct SemiResolvedAddress semiResolve(struct Region* regions, int regionsNum, uintptr_t address) 
// convert address from our address space to
// address suitable fo addr2line 
    struct Region* region;
    struct SemiResolvedAddress res = "", address;
    for(region = regions; region < regions+regionsNum; region++) 
        if(address >= region->start && address < region->end) 
            res.path = region->path;
            res.offset = address - region->start;
        
    
    return res;


void printStacktraceWithLines(unsigned int max_frames)

    int regionsNum;
    fprintf(stderr, "stack trace:\n");

    // storage array for stack trace address data
    void* addrlist[max_frames+1];

    // retrieve current stack addresses
    int addrlen = backtrace(addrlist, sizeof(addrlist) / sizeof(void*));
    if (addrlen == 0) 
        fprintf(stderr, "  <empty, possibly corrupt>\n");
        return;
    
    struct Region* regions = getRegions(&regionsNum); 
    for (int i = 1; i < addrlen; i++)
    
        struct SemiResolvedAddress hres =
                semiResolve(regions, regionsNum, (uintptr_t)(addrlist[i]));
        char syscom[256];
        sprintf(syscom, "addr2line -C -f -p -a -e %s 0x%jx", hres.path, (intmax_t)(hres.offset));
        system(syscom);
    
    free(regions);

【讨论】:

这假定 EXE 中的程序地址从 0 (VMAbase = 0) 开始计数,这对于某些链接器来说并非如此。看我的帖子。

以上是关于如何使用带有行号信息的 gcc 获取 C++ 的堆栈跟踪?的主要内容,如果未能解决你的问题,请参考以下文章

c++:以编程方式将符号转换为代码行号

我的Android进阶之旅NDK开发之在C++代码中使用Android Log打印日志,打印内容带有文件文件名方法名行号 等信息,方便定位日志输出的地方

我的Android进阶之旅NDK开发之在C++代码中使用Android Log打印日志,打印内容带有文件文件名方法名行号 等信息,方便定位日志输出的地方

如何使用带有“-fomp”和“-pg -fprofile-create”的 gcc/gprof 分析 c++?

如何在 eax 和 esi 中调用带有参数的函数。 GCC C++

C++分段错误中的堆算法